this的默认绑定规则总共与下面五种:

  • 默认绑定(严格/非严格)
  • 隐式绑定
  • 显示绑定
  • new绑定
  • 箭头函数绑定

调用位置

什么是调用位置?

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

查找方法:

  • 分析调用栈: 调用位置就是当前正在执行的函数的前一个调用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function a () {
// 当前调用栈是: a
// 因此, 当前调用位置是全局作用域

console.log('a')
b() // b 的调用位置
}

function b () {
// 当前调用栈是: a --> b
// 因此,当前调用位置在a 中

console.log('b')
c() // c 的调用位置
}

function c () {
// 当前调用栈是: a --> b --> c
// 因此,当前调用位置在b中

console.log('c')
}

a(); // a 的调用位置
  • 使用开发者工具
    设置断点,展示当前位置的函数调用列表(call stack)即调用栈,找到栈中的第二个元素,这就是真正的调用位置。

绑定规则

默认绑定

  • 独立函数调用,可以把默认绑定看做是无法运用其他规则时的默认规则,this指向全局对象
  • 严格模式下,不能将全局对象用于默认绑定,this会绑定到undefined。但是在严格模式下进行函数调用不影响this默认绑定,即this可以绑定到全局对象。只有在非严格模式下,this才能绑定到全局对象。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function ex () {
"use strict";

console.log(this.a);
}

var a = 2;

// 调用
ex(); //TypeError: Cannot read property 'a' of undefined

/*======================================*/
function ex () {
console.log(this.a);
}

var a = 2;

(function () {
"use strict";

ex();
})()

隐式绑定

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

1
2
3
4
5
6
7
8
9
10
function ex () {
console.log(this.a);
}

var obj = {
a: 2,
ex: ex
};

obj.ex(); // 2

隐式丢失

被隐式绑定的函数在特定情况下会丢失绑定对象,应用默认绑定,把this绑定到全局对象或者undefined上。

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

var obj = {
a: 2,
ex: ex
}

var exT = obj.ex;
var a = "this global"; // a是全局对象的属性

exT(); // this global

// 虽然exT是obj.ex的一个引用, 但是实际上, 它引用的是ex函数本身
// exT()是一个不带任何修饰的函数调用, 应用默认绑定
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
function ex () {
console.log(this.a);
}

function doEx (fn) {
// fn 其实引用的是ex

fn(); // 调用位置
}

var obj = {
a: 1,
ex: ex
};

var a = "this global";

doEx(obj.ex); // this global

// ======================================

// JS 环境中内置的setTimeout()函数和上面的伪代码类似:
function setTimeout(fn, delay) {
// 等待 delay毫秒
fn(); // 调用位置
}

显示绑定

通过call(···)或者apply(···)方法。第一个参数是一个对象,在调用函数时将这个对象绑定到this上。因为直接指定this的绑定对象,称之为显示绑定。

1
2
3
4
5
6
7
8
9
function ex () {
console.log(this.a);
}

var obj = {
a: 2
};

ex.call(obj); // 2 调用ex时强制把ex的this绑定到obj上

显示绑定无法解决丢失绑定的问题

解决方案:

硬绑定

创建函数bar(),并在它的内部手动调用foo.call(obj),强制把foo的this绑定到了obj。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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
19
20
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
  • ES5内置了Function.prototype.bind,bind会返回一个硬绑定的新函数,用法如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
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

API调用的“上下文”

JS许多内置函数提供了一个可选参数,被称之为“上下文”(context),其作用和bind(..)一样,确保回调函数使用指定的this。这些函数实际上通过call(..)和apply(..)实现了显式绑定。

1
2
3
4
5
6
7
8
9
10
11
12
function foo(el) {
console.log( el, this.id );
}

var obj = {
id: "awesome"
}

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

new 绑定

  • 在JS中,构造函数只是使用new操作符时被调用的普通函数,他们不属于某个类,也不会实例化一个类。
  • 包括内置对象函数(比如Number(..))在内的所有函数都可以用new来调用,这种函数调用被称为构造函数调用。
  • 实际上并不存在所谓的“构造函数”,只有对于函数的“构造调用”。

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

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

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

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

var bar = new foo(2); // bar和foo(..)调用中的this进行绑定
console.log( bar.a ); // 2

手写一个new实现

1
2
3
4
5
6
7
8
9
10
11
12
function create() {
// 创建一个空的对象
var obj = new Object(),
// 获得构造函数,arguments中去除第一个参数
Con = [].shift.call(arguments);
// 链接到原型,obj 可以访问到构造函数原型中的属性
obj.__proto__ = Con.prototype;
// 绑定 this 实现继承,obj 可以访问到构造函数中的属性
var ret = Con.apply(obj, arguments);
// 优先返回构造函数返回的对象
return ret instanceof Object ? ret : obj;
};

使用这个手写的new

1
2
3
4
5
6
7
function Person() {...}

// 使用内置函数new
var person = new Person(...)

// 使用手写的new,即create
var person = create(Person, ...)

代码原理解析:

  • 1、用new Object()的方式新建了一个对象obj

  • 2、取出第一个参数,就是我们要传入的构造函数。此外因为 shift 会修改原数组,所以 arguments会被去除第一个参数

  • 3、将 obj的原型指向构造函数,这样obj就可以访问到构造函数原型中的属性

  • 4、使用apply,改变构造函数this的指向到新建的对象,这样 obj就可以访问到构造函数中的属性

  • 5、返回 obj

箭头函数绑定

ES6新增一种特殊函数类型:箭头函数,箭头函数无法使用上述四条规则,而是根据外层(函数或者全局)作用域(词法作用域)来决定this

  • foo()内部创建的箭头函数会捕获调用时foo()this。由于foo()this绑定到obj1bar(引用箭头函数)的this也会绑定到obj1,箭头函数的绑定无法被修改(new也不行)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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!

ES6之前和箭头函数类似的模式,采用的是词法作用域取代了传统的this机制。

1
2
3
4
5
6
7
8
9
10
11
12
function foo() {
var self = this; // lexical capture of this
setTimeout( function() {
console.log( self.a ); // self只是继承了foo()函数的this绑定
}, 100 );
}

var obj = {
a: 2
};

foo.call(obj); // 2

代码风格统一问题:如果既有this风格的代码,还会使用 seft = this 或者箭头函数来否定this机制。

  • 只使用词法作用域并完全抛弃错误this风格的代码;
  • 完全采用this风格,在必要时使用bind(..),尽量避免使用 self = this 和箭头函数

其实大部分情况下可以用一句话来概括,this总是指向调用该函数的对象

但是对于箭头函数并不是这样,是根据外层(函数或者全局)作用域(词法作用域)来决定this。

对于箭头函数的this总结如下:

  • 箭头函数不绑定this,箭头函数中的this相当于普通变量。

  • 箭头函数的this寻值行为与普通变量相同,在作用域中逐级寻找。

  • 箭头函数的this无法通过bindcallapply来直接修改(可以间接修改)。

  • 改变作用域中this的指向可以改变箭头函数的this。

  • eg. function closure(){()=>{//code }},在此例中,我们通过改变封包环境closure.bind(another)(),来改变箭头函数this的指向。

优先级

1
2
3
4
5
6
7
8
9
10
11
new绑定:this绑定新创建的对象,
var bar = new foo()

显示绑定this:绑定指定的对象,
var bar = foo.call(obj2)

隐式绑定:this绑定上下文对象,
var bar = obj1.foo()

默认绑定:函数体严格模式下绑定到undefined,否则绑定到全局对象。
var bar = foo()

在new中使用硬绑定函数的目的是预先设置函数的一些参数,这样在使用new进行初始化时就可以只传入其余的参数(柯里化)。

1
2
3
4
5
6
7
8
9
10
11
function foo(p1, p2) {
this.val = p1 + p2;
}

// 之所以使用null是因为在本例中我们并不关心硬绑定的this是什么
// 反正使用new时this会被修改
var bar = foo.bind( null, "p1" );

var baz = new bar( "p2" );

baz.val; // p1p2

绑定例外

被忽略的this

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

下面两种情况下会传入null

  • 使用apply(..)来“展开”一个数组,并当作参数传入一个函数
  • bind(..)可以对参数进行柯里化(预先设置一些参数)
1
2
3
4
5
6
7
8
9
10
function foo(a, b) {
console.log( "a:" + a + ",b:" + b );
}

// 把数组”展开“成参数
foo.apply( null, [2, 3] ); // a:2,b:3

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

总是传入null来忽略this绑定可能产生一些副作用。如果某个函数确实使用了this,那默认绑定规则会把this绑定到全局对象中。

更安全的this

安全的做法就是传入一个特殊的对象(空对象),把this绑定到这个对象不会对你的程序产生任何副作用。

JS中创建一个空对象最简单的方法是Object.create(null),这个和{}很像,但是并不会创建Object.prototype这个委托,所以比{}更空。

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

// 我们的空对象
var ø = Object.create( null );

// 把数组”展开“成参数
foo.apply( ø, [2, 3] ); // a:2,b:3

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

间接引用

间接引用下,调用这个函数会应用默认绑定规则。间接引用最容易在赋值时发生。

1
2
3
4
5
6
7
8
9
10
11
// p.foo = o.foo的返回值是目标函数的引用,所以调用位置是foo()而不是p.foo()或者o.foo()
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强制绑定到指定的对象(new除外),防止函数调用应用默认绑定规则。但是会降低函数的灵活性,使用硬绑定之后就无法使用隐式绑定或者显式绑定来修改this
  • 如果给默认绑定指定一个全局对象和undefined以外的值,那就可以实现和硬绑定相同的效果,同时保留隐式绑定或者显示绑定修改this的能力。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 默认绑定规则,优先级排最后
// 如果this绑定到全局对象或者undefined,那就把指定的默认对象obj绑定到this,否则不会修改this
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;
};
}

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

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

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

// 默认绑定,应用软绑定,软绑定把this绑定到默认对象obj
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

思考题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
var num = 1;
var myObject = {
num: 2,
add: function() {
this.num = 3;
(function() {
console.log(this.num);
this.num = 4;
})();
console.log(this.num);
},
sub: function() {
console.log(this.num)
}
}
myObject.add();
console.log(myObject.num);
console.log(num);
var sub = myObject.sub;
sub();

答案有两种情况,分为严格模式和非严格模式。

  • 严格模式下,报错。TypeError: Cannot read property ‘num’ of undefined
  • 非严格模式下,输出:1、3、3、4、4
    解答过程:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
var num = 1;
var myObject = {
num: 2,
add: function() {
this.num = 3; // 隐式绑定 修改 myObject.num = 3
(function() {
console.log(this.num); // 默认绑定 输出 1
this.num = 4; // 默认绑定 修改 window.num = 4
})();
console.log(this.num); // 隐式绑定 输出 3
},
sub: function() {
console.log(this.num) // 因为丢失了隐式绑定的myObject,所以使用默认绑定 输出 4
}
}
myObject.add(); // 1 3
console.log(myObject.num); // 3
console.log(num); // 4
var sub = myObject.sub;// 丢失了隐式绑定的myObject
sub(); // 4