RN-《你不知道的JS》上卷第二部分

第一章:关于 this

下面是一段关于 this 经典误解的代码

1
2
3
4
5
6
7
8
9
10
function foo() {
var a = 2;
this.bar();
}

function bar() {
console.log(this.a) // undefined
}

foo();

第二章:this 全面理解

绑定规则

默认绑定

规则

在非严格模式下,进行独立函数调用像下面这样,this 默认绑定到了 window 上。

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

在严格模式下,则不能将全局对象用于默认绑定,因此 this 会绑定到 undefined,会报错

1
2
3
4
5
6
function foo() {
"use strict"
console.log(this.a);
}
var a = 2;
foo(); // TypeError: Cannot read property 'a' of undefined

这里注意,在严格模式下调用 foo 不影响默认绑定

1
2
3
4
5
6
7
8
function foo() {
console.log(this.a);
}
var a = 2; // 这里不能写在 立即执行函数 后面,因为那时 a === undefined
(function () {
"use strict";
foo(); // 2
})()

间接引用

间接引用最容易在赋值时发生:下面的代码中 p.foo = o.foo 的返回值是目标函数的引用,因此调用位置时 foo() 而不是 p.foo() 或者 o.foo()。这里会应用默认绑定

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

隐式绑定

规则

当函数引用有上下文对象时, 隐式绑定规则会把函数调用中的 this 绑定到这个上下文对象。

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

对象属性引用链中只有上一层或者说最后一层在调用位置中起作用。

1
2
3
4
5
6
7
8
9
10
11
12
13
function foo() {
console.log(this.a);
}
var obj2 = {
a: 2,
foo: foo
}
// 这里的 obj2 必须放在上面,不然会报错,如果放在下面,obj1 中的 obj2 === undefined
var obj1 = {
a: 1,
obj2: obj2
}
obj1.obj2.foo(); // 2

隐式丢失

隐式丢失的问题:丢失绑定对象,应用默认绑定

  1. 函数别名

下面这段代码中虽然 barobj.foo 的一个引用,但实际上,它引用的是 foo 函数本身,因此 bar() 其实是一个不带任何修饰的函数调用,因此应用了默认绑定。

1
2
3
4
5
6
7
8
9
10
11
function foo() {
console.log(this.a);
}
var obj = {
a: 2,
foo: foo
};
var bar = obj.foo; // 函数别名
var a = "Hi, global"

bar() //Hi, global
  1. 函数传参
1
2
3
4
5
6
7
8
9
10
11
12
function foo() {
console.log(this.a);
}
function doFoo(fn) {
fn();
}
var obj = {
a: 2,
foo: foo
}
var a = "global";
doFoo(obj.foo); // global

参数传递其实就是一种隐式赋值,因此我们传入函数时也会被隐式赋值

传入语言内置函数,与上面的结果相同

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

显式绑定

call、apply、bind

1
2
3
4
5
6
7
8
9
10
11
12
function foo() {
console.log(this.a);
}
var obj = {
a: 2
};
var bar = function () {
foo.call(obj);
}
bar(); // 2
setTimeout(bar, 100); // 2
bar.call(window); // 2

绑定之后就不能再修改 this 的指向。

包裹函数

典型的一个应用场景就是创建一个包裹函数,负责接收参数并返回值

1
2
3
4
5
6
7
8
9
10
11
12
function foo(something) {
console.log(this.a, something);
return this.a + something;
}
var obj = {
a: 2
};
var bar = function () {
return foo.apply(obj, arguments);
}
var b = bar(3); // 2 3
console.log(b); // 5

辅助函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function foo(something) {
console.log(this.a, something);
return this.a + something;
}
// 简单的辅助绑定函数
function bind(fn, obj) {
return function () {
return fn.apply(obj, arguments);
}
}
var obj = {
a: 2
};

var bar = bind(foo, obj);

var b = bar(3); // 2 3
console.log(b); // 5

bind

1
2
3
4
5
6
7
8
9
10
11
12
function foo(something) {
console.log(this.a, something);
return this.a + something;
}
var obj = {
a: 2
};

var bar = foo.bind(obj);

var b = bar(3); // 2 3
console.log(b); // 5

new绑定

使用new来调用函数,或者说发生构造函数调用时,会自动执行下面的操作。

  1. 创建(或者说构造)一个全新的对象。
  2. 这个新对象会被执行 [[Prototype]] 连接。
  3. 这个新对象会绑定到函数调用的 this。
  4. 如果函数没有返回其他对象,那么 new 表达式中的函数调用会自动返回这个新对象
1
2
3
4
5
function foo(a) {
this.a = a;
}
var bar = new foo(2);
console.log(bar.a) // 2

优先级

可以按照下面的顺序进行判断:

  1. 函数是否在 new 中调用(new绑定)?如果是的话this绑定的是新创建的对象。var bar = new foo()
  2. 函数是否通过call、apply(显式绑定)或者bind(硬绑定)调用。var bar = foo.call(obj2)
  3. 函数是否在某个上下文对象中调用(隐式绑定)。var bar = obj1.foo()
  4. 如果都不是的话,使用默认绑定。如果在严格模式下,就绑定到 undefined ,否则绑定到全局对象。var bar = foo()

绑定例外

如果把 null 或者 undefined 作为 this 的绑定对象传入 call、apply或者bind,这些值在调用时会被忽略,实际上应用的是默认绑定规则。

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

柯里化

一种非常常见的做法是使用apply()来展开一个数组,并当作参数传入一个函数。类似的,bind() 可以对参数进行柯里化(预先设置一些参数)

1
2
3
4
5
6
7
8
9
10
function foo(a, b) {
console.log(a)
console.log(b);
}
// 把数组展开成参数
foo.apply(null, [2, 3]); // 2 3

// 使用 bind()进行柯里化
var bar = foo.bind(null, 2);
bar(3); // 2 3

这两种方法都需要传入一个参数当做 this 的绑定对象

Object.create(null)

Object.create(null) 和 {} 很像,但是不会创建 Object.prototype 这个委托,所有它比 {} 更空,这样设置比设置为 null 和 undefined 更安全。

1
2
3
4
5
6
7
8
9
10
11
function foo(a, b) {
console.log(a)
console.log(b);
}
// 把数组展开成参数
var k = Object.create(null);
foo.apply(k, [2, 3]); // 2 3

// 使用 bind()进行柯里化
var bar = foo.bind(k, 2);
bar(3); // 2 3

第三章: 对象

属性描述符

1
2
3
4
5
var myObject = {
a: 2
}
Object.getOwnPropertyDescriptor(myObject, "a")
// {value: 2, writable: true, enumerable: true, configurable: true}

创建普通属性时属性描述符会使用默认值,我们也可以使用 Object.defineProperty() 来添加一个新属性或者修改一个已有属性(如果它是 configurable)并对特性进行设置。一般来说不会使用

1
2
3
4
5
6
7
8
9
10
var myObject = {}

Object.defineProperty(myObject, "a", {
value: 2,
writable: true,
enumerable: true,
configurable: true
})

console.log(myObject.a) // 2

writable

决定是否可以修改属性的值。

1
2
3
4
5
6
7
8
9
10
11
12
var myObject = {}

Object.defineProperty(myObject, "a", {
value: 2,
writable: false, // 不可写!
enumerable: true,
configurable: true
})

myObject.a = 3;

console.log(myObject.a) // 2

在上面的非严格模式下,只是修改静默失败,如果在严格模式下,这种方法会报错。

configurable

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var myObject = {}

Object.defineProperty(myObject, "a", {
value: 2,
writable: true,
enumerable: true,
configurable: false // 不可配置
})
myObject.a = 3;

Object.defineProperty(myObject, "a", {
value: 3,
writable: true,
enumerable: true,
configurable: true
})
// TypeError: Cannot redefine property: a at Function.defineProperty (<anonymous>)

可以看到把 configurable 修改为 false 是单向操作,无法撤销!

有一个小小的例外:即便属性是 configurable: false,我们还是可以把 writable 的状态由 true 改为 false ,但是无法由 false 改为 true

除了无法修改, configurable: false 还会禁止删除这个属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var myObject = {
a: 2
}
console.log(myObject.a); // 2
delete myObject.a;
console.log(myObject.a); // undefined

Object.defineProperty(myObject, "a", {
value: 2,
writable: true,
enumerable: true,
configurable: false // 不可配置
})

console.log(myObject.a); // 2
delete myObject.a;
console.log(myObject.a); // 2

enumerable

这个是控制属性是否会出现在对象的属性枚举中,比如 for in 循环,如果把 enumerable 设置为 false,这个属性就不会出现在枚举中,虽然仍然可以正常访问它,设置为 true 就会让它出现在枚举中。

下面的代码可以检验是否为可枚举:

propertyIsEnumerable() 会检查给定的属性名是否直接存在于对象中(而不是在原型链上)并且满足 enumerable:true

Object.keys() 会返回一个数组,包含所有可枚举属性,Object.getOwnPropertyNames() 会返回一个数组,包含所有属性,无论它们是否可枚举。这两个方法都只会查找对象直接包含的属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var myObject = {}
Object.defineProperty( myObject, "a", {
enumerable: true, // 可枚举
value: 2
})

Object.defineProperty( myObject, "b", {
enumerable: false, // 不可枚举
value: 3
})

myObject.propertyIsEnumerable("a") // true
myObject.propertyIsEnumerable("b") // false

Object.keys(myObject) // ["a"]
Object.getOwnPropertyNames(myObject) // ["a", "b"]

存在性

1
2
3
4
5
var myObject = {
a: undefined
}
console.log(myObject.a) // undefined
console.log(myObject.b) // undefined

可以应用 in 和 hasOwnProperty 来进行判断属性是否真的存在于对象中。

in hasOwnProperty

1
2
3
4
5
6
7
8
var myObject = {
a: 2
}
"a" in myObject // true
"b" in myObject // false

myObject.hasOwnProperty("a") // true
myObject.hasOwnProperty("b") // false

in 操作符会检查属性是否在对象及其 [[Prototype]]原型链 中,hasOwnProperty() 只会检查属性是否在 myObject对象中,不会检查 [[Prototype]]链。