读《你不知道的js——上卷(2)》
读书笔记
第二部分 this 和对象原型
第 1 章 关于 this
this:Javascript 关键字,被自动定义在所有函数的作用域中。
this: 是在==运行时绑定==的,并不是在编写时绑定,只取决于函数调用方式。
this: 既不指向自身,也不指向函数的词法作用域。
this 在任何情况下==都不指向==函数的词法作用域
1.2 对 this 的误解
1.2.1 指向自身(误解)
我们想要记录函数 foo 被调用的次数:
1 | function foo(num) { |
执行 foo.count = 0 时,的确向函数对象 foo 添加了一个属性 count。但是函数内部代码 this.count 中的 this 并不是指向那个函数对象,所以虽然属性名相同,根对象却并不相同。
具名函数可以使用函数名指向自身,匿名函数使用 arguments. callee 指向自身(这个已被弃用?应避免使用匿名函数)。
1.2.2 它的作用域(误解)
第二种常见的误解是,this 指向函数的作用域。在某种情况下它是正确的,但是在其他情况下它却是错误的。
在 JavaScript 内部,作用域确实和对象类似,可见的标识符都是它的属性。但是作用域“对象”无法通过 JavaScript 代码访问,它存在于 JavaScript 引擎内部。
第 2 章 this 全面解析
2.1 调用位置
调用位置:函数在代码中被调用的 位置(而不是声明的位置)。
调用栈:为了到达当前执行位置所调用的所有函数(可以把调用栈想象成一个函数调用链)。
看如下代码:
1 | function baz() { |
2.2 绑定规则
执行过程中调用位置如何决定 this 的绑定对象?首先要找到「调用位置」,然后判断用了下列那一条规则。
2.2.1 默认绑定
最常用的函数调用类型:独立函数调用。可以把这条规则看作是无法应用其他规则时的默认规则。
1 | function foo () { |
因为 foo 是使用不带任何修饰的函数引用进行调用的,只能应用「默认绑定」,所以 this 指向全局对象。
严格模式下,全局对象将无法使用默认绑定,因此 this 会绑定 到 undefined:
1 | // 运行在严格模式下 |
注意下面代码:
1 | // 在严格模式下调用 |
foo 运行在非严格模式下,this 才会默认绑定到全局对象。foo 在非严格模式下调用,不会影响 this 的绑定。
2.2.2 隐式绑定
看代码:
1 | function foo() { |
当函数引用有上下文对象时,隐式绑定规则会把函数调用中的 this 绑定到这个上下文对象。foo 的调用位置会使用 obj 上下文来引用函数,所以 this 被绑定到了 obj 上。
隐式丢失
「隐式绑定」 的函数会丢失绑定对象,会应用默认绑定,从而吧 this 绑定到全局或者 undefined 上。
例:
1 | function foo() { |
bar 是 obj.foo 的一个引用,但是实际上,它引用的是 foo 函数本身,因此此时的 bar() 其实是一个不带任何修饰的函数调用,因此应用了默认绑定。
代码2:
参数传递是一种隐式赋值
1 | function foo() { |
2.2.3 显式绑定
使用函数的
call、bind和apply方法;
1 | function foo() { |
如果你传入了一个原始值(字符串类型、布尔类型或者数字类型)来当作 this 的绑定对象,这个原始值会被转换成它的对象形式(也就是 new String(..)、new Boolean(..) 或者 new Number(..))。这通常被称为「装箱」.
显式绑定仍然存在「丢失绑定」问题,但有其他解决方案。
1. 硬绑定
在 bar 内部将 foo 的 this强制绑定到 obj 上,无论后面如何调用 bar,都不会改变 this 的绑定。
1 | function foo() { |
bind(..) 会返回一个新函数,需要我们手动调用。
1 | var a ={ |
2. API 调用的「上下文」
第三方库的许多函数,以及
JavaScript语言和宿主环境中许多新的内置函数,都提供了一 个可选的参数,通常被称为「上下文」(context),其作用和bind(..)一样,确保你的回调 函数使用指定的this。
代码:
1 | function foo(el) { |
2.2.4 new 绑定
使用构造调用的时候,this会自动绑定在new期间创建的对象上
- 创建(或者说构造)一个全新的对象。
- 这个新对象会被执行[[原型]]连接。
- 这个新对象会绑定到函数调用的this。
- 如果函数没有返回其他对象,那么new表达式中的函数调用会自动返回这个新对象。
2.3 优先级
new 绑定 > 显示绑定 > 隐式绑定 > 默认绑定
2.4 例外绑定
如果你把 null 或者 undefined 作为 this 的绑定对象传入 call、apply 或者 bind,这些值
在调用时会被忽略,实际应用的是「默认绑定」规则:
通常 apply 通过传入 null 来展开数组
1 | function foo(a,b) { |
2.5 this 词法
箭头函数中 this 由外层(函数或全局)作用域来决定.
箭头函数的绑定无法被修改。(new 也不行!).
如下代码:foo() 内部创建的箭头函数会捕获调用时 foo() 的 this。由于 foo() 的 this 绑定到 obj1, bar (引用箭头函数)的 this 也会绑定到 obj1.
1 | function foo() { |
第 3 章 对象
3.2 类型
string、number、null、boolean、undefined、object
typeof null 时会返回字符串 object。实际上,null 本身是基本类型。
3.3 内容
.a 语法通 常被称为“属性访问”
["a"] 语法通 常被称为“键访问”
对象中,属性名永远都是字符串,属性名表达式如果是一个对象,默认情况下会自动将对象转为字符串 [object Object]。
3.3.4 复制
1 | // 深复制 |
ES6 定义了 Object.assign(targetObject, sourceObject) 方法来实现浅复制.
3.3.6 不变性
希望属性或对象是不可变的。「所有的方法创建的都是浅不变性,只会影响目标对象和 它的直接属性」,如果目标对象引用了其他对象(数组,对象,函数等),==其他对象的内容不受影响==。
可通过下列方法实现深不可变性(即 如果目标对象引用了其他对象(数组,对象,函数等),其他对象的内容==会受影响==)
1、对象常量
结合 writable:false 和 configurable:false 实现。
2、禁止扩展
禁止一个对象添加新属性并且保留已有属性,可以使用
Object.prevent Extensions(..)
1 | var myObject = { |
3、密封
Object.seal(..)会创建一个“密封”的对象,这个方法实际上会在一个现有对象上调用Object.preventExtensions(..)并把所有现有属性标记为configurable:false。
密封之后不仅不能添加新属性,也不能重新配置或者删除任何现有属性(虽然可以 修改属性的值)
4、冻结
Object.freeze(..)会创建一个冻结对象,这个方法实际上会在一个现有对象上调用Object.seal(..)并把所有“数据访问”属性标记为writable:false。
3.3.9 Getter和Setter
对象默认的
[[Put]]和[[Get]]操作分别可以控制属性值的设置和获取。在 ES5 中可以使用getter和setter部分改写默认操作,但是只能应用在==单个属性==上,无法 应用在整个对象上
3.3.10 存在性
in 操作符会检查属性是否在对象及其 [[Prototype]] 原型链中.
hasOwnProperty(..) 只会检查属性是否在对象自身中,不会检查 [[Prototype]] 链.
第 4 章 混合对象「类」
面向类的设计模式:实例化( instantiation )、继承( inheritance )、多态( polymorphism )。
4.1 类理论
类意味着复制。
4.1.1 “类”设计模式
你可能从来没把类作为设计模式来看待,因为讨论得最多的是面向对象设计模式
类不是必须的编程基础,而是一种可选的代码抽象。
4.1.2 JavaScript中的“类”
JavaScript中实际上没有「类」,只有一些近似类的语法元素(比如new、instanceof以及 ES6 中的class关键字。)
4.2 类的机制
类仅仅是一个抽象的表示,需要先实例化才能对其进行操作。
4.3 类的继承
多态并不表示子类和父类有关联,子类得到的只是父类的一份副本。类的继承其实就是==复制==。
4.4 混入
在继承或者实例化时,JavaScript 的对象机制并不会自动执行复制行为。简单来说, JavaScript 中只有对象,并不存在可以被实例化的“类”。在其他语言中类表现出来的都是复制行为,因此 JavaScript 开发者也想出了一个方法来 模拟类的复制行为,这个方法就是==混入==
4.5 小结
传统的类被实例化时,它的行为会被复制到实例中。类被继承时,行为也会被复制到子类 中。
多态(在继承链的不同层次名称相同但是功能不同的函数)看起来似乎是从子类引用父 类,但是本质上引用的其实是复制的结果。
JavaScript 并不会(像类那样)自动创建对象的副本。
混入模式(无论显式还是隐式)可以用来模拟类的复制行为,但是通常会产生丑陋并且脆 弱的语法,比如显式伪多态(OtherObj.methodName.call(this, …)),这会让代码更加难 懂并且难以维护。
显式混入实际上无法完全模拟类的复制行为,因为对象只能复制引用.
第 5 章 原型
5.1 [[Prototype]]
JavaScript内置属性,是对其他对象的引用,对象在创建时[[Prototype]]属性都会被赋予一个非空的值.
当你通过各种语法进行属性查找时都会查找 [[Prototype]] 链,直到找到属性或者
查找完整条原型链。
5.1.1 Object.prototype
[[Prototype]] 链最终都会指向内置的 Object.prototype
5.1.2 属性设置和屏蔽
1 | newObject.foo = 'demo' |
上面的代码,如果 newObject 中存在 foo 属性,则上述语句只会修改已有属性。
如果 foo 不存在 newObject 上,则开始遍历 [[Prototype]],如果在原型链上找不到 foo,则将 foo 直接添加在 newObject 上。
如果 foo 即存在于 newObject 上,又存在于 [[Prototype]] 上,[[Prototype]] 上的 foo 会被屏蔽。
如果 foo 不存在于 newObject 上,存在于 [[Prototype]] 上,则有三种情况:
- 如果
[[Prototype]]上存在foo且 ==没有被标记为只读==,那就直接在newObject上新添加一个foo属性; - 如果
[[Prototype]]上存在foo且 ==被标记为只读==,严格模式下会报错,非严格模式下会忽略。 - 如果
[[Prototype]]上存在foo同时它是一个setter,那么foo不会被添加到newObject上,也不会重新定义foo这个setter。
有些情况会发生隐式屏蔽:
1 | var anotherObject = { a:2 |
尽管 myObject.a++ 看起来应该(通过委托)查找并增加 anotherObject.a 属性,但是别忘 了 ++ 操作相当于 myObject.a = myObject.a + 1。因此 ++ 操作首先会通过 [[Prototype]] 查找属性 a 并从 anotherObject.a 获取当前属性值 2,然后给这个值加 1,接着用 [[Put]] 将值 3 赋给 myObject 中新建的屏蔽属性 a.
5.4 对象关联
5.4.1 创建关联
Object.create()
1 | var foo = { |
Object.create(..) 会创建一个新对象( bar )并把它关联到我们指定的对象( foo )
用 new 的构造函数调用会生成 .prototype 和 .constructor 引用
部分实现 Object. create(..) 的功能:
1 | if (!Object.create) { |
5.4 小结
关联两个对象最常用的方法是使用 new 关键词进行函数调用,会把新对象的 .prototype 属性关联到“其他对象”。
[[Prototype]] 机制就是指对象中的一个内部链接引用另一个对象,这个机制的本质就是对象之间的关联关系。
第 6 章 行为委托
6.6 小结
行为委托认为对象之间是兄弟关系,互相委托,而不是父类和子类的关系。
JavaScript 的 [[Prototype]] 机制本质上就是行为委托机制