《你不知道的JavaScript》读书笔记(二)

this和对象原型

this既不指向函数自身也不指向函数的词法作用域;
this实际上是在函数被调用时发生的绑定,它指向什么完全取决于函数在哪里被调用。

默认绑定(调用位置)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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的调用位置

隐式绑定

当函数引用有上下文对象时,隐式绑定规则会把函数调用中的this绑定到这个上下文对象。并且对象属性引用链中只有上一次或者说最后一层在调用位置中起作用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function foo() {
console.log(this.a);
}

var obj2 = {
a: 42,
foo: foo
};

var obj1 = {
a:2,
obj2:obj2
}

obj1.obj2.foo(); //42

隐式丢失

被隐式绑定的函数会丢失绑定对象,也就是说它会应用默认绑定,从而把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 = "opps,global"; // a是全局对象的属性
bar(); //"opps,global"

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

另外:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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); //"opps, global"

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

显式绑定

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

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

// 调用foo(..)时把this绑定到obj
[1,2,3].forEach(foo,obj);
// hahaha 1 hahaha 2 hahaha 3

new绑定

构造函数:在JavaScript中,构造函数只是一些使用new操作符时调用的函数。它们并不会属于某个类,也不会实例化一个类。实际上他,它们甚至都不能说是一种特殊的函数类型,它们只是被new操作符调用的普通函数而已。
实际上并不存在所谓的“构造函数”,只有对于函数的“构造调用”。

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

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

    var bar = new foo(2);
    console.log(bar.a); // 2

使用new来调用foo(..)时,我们会构造一个新对象并把它绑定到foo(..)调用中的this上。

优先级

new绑定>显式绑定>隐式绑定>默认绑定
判断this

  • 函数是否在new中调用(new绑定)?如果是的话this绑定的是新创建的对象。

    1
    var bar = new foo();
  • 函数是否通过call、apply(显式绑定)或者硬绑定调用?如果是的话,this绑定的是指定的对象。

    1
    var bar = foo.call(obj2);
  • 函数是否在某个上下文对象中调用(隐式绑定)?如果是的话,this绑定的是那个上下文对象。

    1
    var bar = obj1.foo()
  • 如果都不是的话,使用默认绑定。如果在严格模式下,就绑定到undefined,否则绑定到全局对象。

    1
    var bar = foo();

忽略this

常见的做法是把null或者undefined传入callapply或者bind,这些值在调用时会被忽略,实际应用的是默认绑定规则,但是将this绑定到全局变量,有可能会导致严重后果(修改全局变量)。
安全的做法:创建一个“DMZ”对象——空的非委托对象,对于this的使用就会被限制在这个空对象中,从而不会对全局对象产生任何影响。

在JavaScript创建一个空对象最简单的方法是Object.create(null),它和{}很像但是不会创建Object.prototype这个委托,所以它比{}“更空”:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function foo(a, b) {
console.log("a:" + a + ",b:" + b);
}

// DMZ对象
var dmz = Object.create(null);

// 把数组展开成参数
foo.call(dmz, [2, 3]);
// a: 2, b: 3

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

间接引用

间接引用最容易在赋值时发生:

1
2
3
4
5
6
7
8
9
10
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

赋值表达式
p.foo = o.foo的返回值是目标函数的引用,因此调用位置是foo()而不是p.foo()或者o.foo()。这里会应用默认绑定。

软绑定

硬绑定 bind(..)会降低函数的灵活性,使用硬绑定之后就无法使用隐式绑定或者显示绑定来修改this(能用new绑定)。
可以通过一种被成为软绑定的方法达到我们想要实现的效果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
if (!Function.prototype.softBind) {
Function.prototype.softBind = function (obj) {
var fn = this;
// 捕获所有curried参数
var curried = [].slice.call(arguments, 1);
var bound = function () {
return fn.apply(
(!this || this === (window || global)) ?
obj : this, curried.concat.apply(curried, arguments)
);
};
bound.prototype = Object.create(fn.prototype);
return bound;
}
}

他会对指定的函数进行封装,首先检查调用时的this,如果this绑定到全局对象或者undefined,那就把指定的默认对象obj绑定到this,否则不会修改this
js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function foo() {
console.log("name: " + this.name);
}

var obj = { name: "obj" };
var obj2 = { name: "obj2" };
var obj3 = { name: "obj3" };

var fooOBJ = foo.softBind(obj);

fooOBJ(); //name: obj

obj2.foo = foo.softBind(obj);
obj2.foo(); //name: obj2 <---- 看!!!

fooOBJ.call(obj3); //name: obj3 <---- 看!

setTimeout(obj2.foo, 10);
// name: obj <---- 应用了软绑定

软绑定版本的foo()可以手动将this绑定到obj2或者obj3上,但如果应用默认绑定,则会将this绑定到obj

箭头函数中的this

箭头函数不适用this的四种标准规则,而是根据外层(函数或者全局)作用域来决定this。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function foo() {
return (a) => {
console.log(this.a); //this继承自foo()
}
}

var obj1 = {
a: 2
};

var obj2 = {
a: 3
};

var bar = foo.call(obj1);
bar.call(obj2); //2

foo()内部创建的箭头函数会捕获调用时foo()的this。由于foo()的this绑定到obj1,bar(引用箭头函数)的this也会绑定到obj1,箭头函数的绑定无法被修改。(new也不行!)

0%