读《你不知道的js——上卷(2)》

读书笔记

第二部分 this 和对象原型

第 1 章 关于 this

thisJavascript 关键字,被自动定义在所有函数的作用域中。

this: 是在==运行时绑定==的,并不是在编写时绑定,只取决于函数调用方式。

this: 既不指向自身,也不指向函数的词法作用域。

this 在任何情况下==都不指向==函数的词法作用域

1.2 对 this 的误解

1.2.1 指向自身(误解)

我们想要记录函数 foo 被调用的次数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
function foo(num) {
console.log( "foo: " + num );
// 记录 foo 被调用的次数
this.count++;
data.count++;
}

foo.count = 0;

var data = {
count: 0
}

var i;

for (i=0; i<10; i++) {
if (i > 5) {
foo( i );
// foo.call( foo, i ) 可以确保 this 指向函数对象 foo 本身
}
}
// foo: 6
// foo: 7
// foo: 8
// foo: 9
// foo 被调用了多少次?
console.log( foo.count ); // 0
// 这里利用的词法作用域
console.log( data.count ); // 4

执行 foo.count = 0 时,的确向函数对象 foo 添加了一个属性 count。但是函数内部代码 this.count 中的 this 并不是指向那个函数对象,所以虽然属性名相同,根对象却并不相同。

具名函数可以使用函数名指向自身,匿名函数使用 arguments. callee 指向自身(这个已被弃用?应避免使用匿名函数)

1.2.2 它的作用域(误解)

第二种常见的误解是,this 指向函数的作用域。在某种情况下它是正确的,但是在其他情况下它却是错误的。

JavaScript 内部,作用域确实和对象类似,可见的标识符都是它的属性。但是作用域“对象”无法通过 JavaScript 代码访问,它存在于 JavaScript 引擎内部。

第 2 章 this 全面解析

2.1 调用位置

调用位置:函数在代码中被调用的 位置(而不是声明的位置)。

调用栈:为了到达当前执行位置所调用的所有函数(可以把调用栈想象成一个函数调用链)。

看如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function baz() {
// 当前调用栈是:baz
// 因此,当前调用位置是全局作用域
console.log( "baz" );
bar(); // <-- bar 的调用位置
}
function bar() {
// 当前调用栈是 baz -> bar
// 因此,当前调用位置在 baz 中
console.log( "bar" );
foo(); // <-- foo 的调用位置
}
function foo() {
// 当前调用栈是 baz -> bar -> foo
// 因此,当前调用位置在 bar 中
console.log( "foo" );
}
baz(); // <-- baz 的调用位置

2.2 绑定规则

执行过程中调用位置如何决定 this 的绑定对象?首先要找到「调用位置」,然后判断用了下列那一条规则。

2.2.1 默认绑定

最常用的函数调用类型:独立函数调用。可以把这条规则看作是无法应用其他规则时的默认规则。

1
2
3
4
5
6
function foo () {
console.log(this.a)
}
var a = 1;
foo() // 2

因为 foo 是使用不带任何修饰的函数引用进行调用的,只能应用「默认绑定」,所以 this 指向全局对象。

严格模式下,全局对象将无法使用默认绑定,因此 this 会绑定 到 undefined

1
2
3
4
5
6
7
// 运行在严格模式下
function foo() {
"use strict";
console.log( this.a );
}
var a = 2;
foo(); // TypeError: this is undefined

注意下面代码:

1
2
3
4
5
6
7
8
9
// 在严格模式下调用
function foo() {
console.log( this.a );
}
var a = 2;
(function(){
"use strict";
foo(); // 2
})();

foo 运行在非严格模式下,this 才会默认绑定到全局对象。
foo 在非严格模式下调用,不会影响 this 的绑定。

2.2.2 隐式绑定

看代码:

1
2
3
4
5
6
7
8
function foo() { 
console.log( this.a );
}
var obj = {
a: 2,
foo: foo
};
obj.foo(); // 2

当函数引用有上下文对象时,隐式绑定规则会把函数调用中的 this 绑定到这个上下文对象foo 的调用位置会使用 obj 上下文来引用函数,所以 this 被绑定到了 obj 上。

隐式丢失

「隐式绑定」 的函数会丢失绑定对象,会应用默认绑定,从而吧 this 绑定到全局或者 undefined 上。

例:

1
2
3
4
5
6
7
8
9
10
function foo() { 
console.log( this.a );
}
var obj = {
a: 2,
foo: foo
};
var bar = obj.foo; // 函数别名!
var a = "oops, global"; // a 是全局对象的属性
bar(); // "oops, global"

barobj.foo 的一个引用,但是实际上,它引用的是 foo 函数本身,因此此时的 bar() 其实是一个不带任何修饰的函数调用,因此应用了默认绑定。

代码2:

参数传递是一种隐式赋值

1
2
3
4
5
6
7
8
9
10
11
12
13
function foo() { 
console.log( this.a );
}
function doFoo(fn) {
// fn 其实引用的是 foo
fn(); // <-- 调用位置!
}
var obj = {
a: 2,
foo: foo
};
var a = "oops, global"; // a 是全局对象的属性
doFoo( obj.foo ); // "oops, global"

2.2.3 显式绑定

使用函数的 callbindapply 方法;

1
2
3
4
5
6
7
8
9
10
11
12
13
function foo() { 
console.log( this.a );
}
var obj = {
a:2
};
foo.call( obj ); // 2
// 在调用 foo 时,将 foo 的 this 绑定到 obj 上

function foo() {
console.log( this );
}
foo.call( 1 );// Number {1}

如果你传入了一个原始值(字符串类型、布尔类型或者数字类型)来当作 this 的绑定对象,这个原始值会被转换成它的对象形式(也就是 new String(..)new Boolean(..) 或者 new Number(..))。这通常被称为「装箱」.

显式绑定仍然存在「丢失绑定」问题,但有其他解决方案。

1. 硬绑定

在 bar 内部将 foo 的 this强制绑定到 obj 上,无论后面如何调用 bar,都不会改变 this 的绑定。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function foo() { 
console.log( this.a );
}
var obj = {
a:2
};
var bar = function() {
foo.call( obj );
};
bar(); // 2
setTimeout( bar, 100 ); // 2
// 硬绑定的 bar 不可能再修改它的 this
bar.call( window ); // 2

bind(..) 会返回一个新函数,需要我们手动调用。

1
2
3
4
5
6
7
8
9
var a ={
name : "Cherry",
fn : function (a,b) {
console.log( a + b)
}
}

var b = a.fn;
b.bind(a,1,2)() // 3

2. API 调用的「上下文」

第三方库的许多函数,以及 JavaScript 语言和宿主环境中许多新的内置函数,都提供了一 个可选的参数,通常被称为「上下文」(context),其作用和 bind(..) 一样,确保你的回调 函数使用指定的 this

代码:

1
2
3
4
5
6
7
8
9
function foo(el) {
console.log( el, this.id );
}
var obj = {
id: "awesome"
};
// 调用 foo(..) 时把 this 绑定到 obj
[1, 2, 3].forEach( foo, obj );
// 1 awesome 2 awesome 3 awesome

2.2.4 new 绑定

使用构造调用的时候,this会自动绑定在new期间创建的对象上

  1. 创建(或者说构造)一个全新的对象。
  2. 这个新对象会被执行[[原型]]连接。
  3. 这个新对象会绑定到函数调用的this。
  4. 如果函数没有返回其他对象,那么new表达式中的函数调用会自动返回这个新对象。

2.3 优先级

new 绑定 > 显示绑定 > 隐式绑定 > 默认绑定

2.4 例外绑定

如果你把 null 或者 undefined 作为 this 的绑定对象传入 callapply 或者 bind,这些值
在调用时会被忽略,实际应用的是「默认绑定」规则:

通常 apply 通过传入 null 来展开数组

1
2
3
4
5
function foo(a,b) {
console.log( "a:" + a + ", b:" + b );
}
// 把数组“展开”成参数
foo.apply( null, [2, 3] ); // a:2, b:3

2.5 this 词法

箭头函数中 this 由外层(函数或全局)作用域来决定.

箭头函数的绑定无法被修改。(new 也不行!).

如下代码:foo() 内部创建的箭头函数会捕获调用时 foo()this。由于 foo()this 绑定到 obj1bar (引用箭头函数)的 this 也会绑定到 obj1.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function foo() {
// 返回一个箭头函数
return (a) => {
//this 继承自 foo()
console.log( this.a );
};
}
var obj1 = {
a:2
};
var obj2 = {
a:3
}
var bar = foo.call( obj1 );
bar.call( obj2 ); // 2, 不是 3 !

第 3 章 对象

3.2 类型

stringnumbernullbooleanundefinedobject

typeof null 时会返回字符串 object。实际上,null 本身是基本类型。

3.3 内容

.a 语法通 常被称为“属性访问”

["a"] 语法通 常被称为“键访问”

对象中,属性名永远都是字符串,属性名表达式如果是一个对象,默认情况下会自动将对象转为字符串 [object Object]

3.3.4 复制

1
2
// 深复制
var newObj = JSON.parse( JSON.stringify( someObj ) );

ES6 定义了 Object.assign(targetObject, sourceObject) 方法来实现浅复制.

3.3.6 不变性

希望属性或对象是不可变的。「所有的方法创建的都是浅不变性,只会影响目标对象和 它的直接属性」,如果目标对象引用了其他对象(数组,对象,函数等),==其他对象的内容不受影响==。

可通过下列方法实现深不可变性(即 如果目标对象引用了其他对象(数组,对象,函数等),其他对象的内容==会受影响==)

1、对象常量

结合 writable:false 和 configurable:false 实现。

2、禁止扩展

禁止一个对象添加新属性并且保留已有属性,可以使用 Object.prevent Extensions(..)

1
2
3
4
5
6
7
8
9
10
var myObject = { 
a:2
};
Object.preventExtensions( myObject );
myObject.b = 3;
myObject.b; // undefined

//在非严格模式下,创建属性 b 会静默失败。
// 在严格模式下,将会抛出 TypeError 错误。

3、密封

Object.seal(..) 会创建一个“密封”的对象,这个方法实际上会在一个现有对象上调用 Object.preventExtensions(..) 并把所有现有属性标记为 configurable:false

密封之后不仅不能添加新属性,也不能重新配置或者删除任何现有属性(虽然可以 修改属性的值)

4、冻结

Object.freeze(..) 会创建一个冻结对象,这个方法实际上会在一个现有对象上调用 Object.seal(..) 并把所有“数据访问”属性标记为 writable:false

3.3.9 Getter和Setter

对象默认的 [[Put]][[Get]] 操作分别可以控制属性值的设置和获取。在 ES5 中可以使用 gettersetter 部分改写默认操作,但是只能应用在==单个属性==上,无法 应用在整个对象上

3.3.10 存在性

in 操作符会检查属性是否在对象及其 [[Prototype]] 原型链中.

hasOwnProperty(..) 只会检查属性是否在对象自身中,不会检查 [[Prototype]] 链.

第 4 章 混合对象「类」

面向类的设计模式:实例化( instantiation )、继承( inheritance )、多态( polymorphism )。

4.1 类理论

类意味着复制。

4.1.1 “类”设计模式

你可能从来没把类作为设计模式来看待,因为讨论得最多的是面向对象设计模式

类不是必须的编程基础,而是一种可选的代码抽象。

4.1.2 JavaScript中的“类”

JavaScript 中实际上没有「类」,只有一些近似类的语法元素(比如 newinstanceof 以及 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]] 上,则有三种情况:

  1. 如果 [[Prototype]] 上存在 foo且 ==没有被标记为只读==,那就直接在 newObject 上新添加一个 foo 属性;
  2. 如果 [[Prototype]] 上存在 foo 且 ==被标记为只读==,严格模式下会报错,非严格模式下会忽略。
  3. 如果 [[Prototype]] 上存在 foo 同时它是一个 setter,那么 foo 不会被添加到 newObject 上,也不会重新定义 foo 这个 setter

有些情况会发生隐式屏蔽:

1
2
3
4
5
6
7
8
9
10
11
12
var anotherObject = { a:2
};
var myObject = Object.create( anotherObject );
anotherObject.a; // 2
myObject.a; // 2
anotherObject.hasOwnProperty( "a" ); // true
myObject.hasOwnProperty( "a" ); // false
myObject.a++; // 隐式屏蔽!
anotherObject.a; // 2
myObject.a; // 3
myObject.hasOwnProperty( "a" ); // true

尽管 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
2
3
4
5
6
7
var foo = {
something: function() {
console.log( "Tell me something good..." );
}
};
var bar = Object.create( foo );
bar.something(); // Tell me something good...

Object.create(..) 会创建一个新对象( bar )并把它关联到我们指定的对象( foo )

new 的构造函数调用会生成 .prototype.constructor 引用

部分实现 Object. create(..) 的功能:

1
2
3
4
5
6
if (!Object.create) { 
Object.create = function(o) {
function F(){}
F.prototype = o;
return new F();
}; }

5.4 小结

关联两个对象最常用的方法是使用 new 关键词进行函数调用,会把新对象的 .prototype 属性关联到“其他对象”。

[[Prototype]] 机制就是指对象中的一个内部链接引用另一个对象,这个机制的本质就是对象之间的关联关系。

第 6 章 行为委托

6.6 小结

行为委托认为对象之间是兄弟关系,互相委托,而不是父类和子类的关系。

JavaScript 的 [[Prototype]] 机制本质上就是行为委托机制