读《你不知道的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]]
机制本质上就是行为委托机制