2

最近重温了一遍《你不知道的JavaScript--上卷》,其中第二部分关于this的讲解让我收获颇多,所以写一篇读书笔记记录总结一番。

消除误解--this指向自身

由于this的英文释义,许多人都会将其理解成指向函数自身(JavaScript 中的所有函数都
是对象),但是实际上this并不像我们所想的那样指向函数自身,我们可以通过下面的栗子验证一下~

function foo(num) {
    console.log( "foo: " + num );
    // 记录foo 被调用的次数
    this.count++;
}
foo.count = 0;
var i;
for (i=0; i<10; i++) {
    if (i > 5) {
        foo( i );
    }
}
// foo: 6
// foo: 7
// foo: 8
// foo: 9
// foo 被调用了多少次?
console.log( foo.count ); // 0 -- WTF?

上述栗子的本意是想记录foo被调用的次数

假设this指向函数本身,那么this.countfoo.count应该是foo函数对象的同一个属性,那么最终得到的foo.count应该是4

然而实际上,最终得到的foo.count0,也就是说foo.count初始化之后就没有再改变过了,所以this.countfoo.count是相互独立的,互不影响;所以结论是:this并不是指向函数本身

那么这个里面的this到底是指向什么呢?你可以思考一下,写下你的答案。然后继续往后看,后面你会得到答案的~~想马上验证可以拖到最后...

this到底是什么

this的确定是在Execution Context的创建阶段,而Execution Context的创建发生在浏览器第一次加载script的时候或者调用函数的时候----具体可参见之前写过的一篇文章JavaScript基础系列---执行环境与作用域链

所以this 是在运行时进行绑定的,并不是在编写时绑定,它的上下文取决于函数调用时的各种条件,this的绑定和函数声明的位置没有任何关系,只取决于函数的调用方式this的指向并没有一个固定的说法,需要分情况而论。

要想明确this指向什么,需要通过寻找函数的调用位置来判断函数在执行过程中会如何绑定this,从而确定this的指向。

寻找调用位置

寻找调用位置就是寻找“函数被调用的位置”,但是做起来并没有这么简单,因为某些编程模式可能会隐藏真正的调用位置,这种时候很容易出错。

最重要的是要分析调用栈(就是为了到达当前执行位置所调用的所有函数),我们关心的调用位置就在当前正在执行的函数的前一个调用中,下面用栗子来帮助理解:

function baz() {
    debugger
    // 当前调用栈是:baz
    // 因此,当前调用位置是全局作用域
    console.log( "baz" );
    bar(); // <-- bar 的调用位置
}
function bar() {
    debugger
    // 当前调用栈是baz -> bar
    // 因此,当前调用位置在baz 中
    console.log( "bar" );
    foo(); // <-- foo 的调用位置
}
function foo() {
    debugger
    // 当前调用栈是baz -> bar -> foo
    // 因此,当前调用位置在bar 中
    console.log( "foo" );
}
baz(); // <-- baz 的调用位置

如果条件允许,可以使用开发者工具进行观察,将会更加直观。

baz函数是在全局作用域中调用的,baz函数的调用栈为baz,所以baz函数的调用位置是全局作用域

clipboard.png

bar函数是在baz函数中调用的,bar函数的调用栈为baz -> bar,当正在执行的是bar函数时,其前一个调用是baz,所以bar函数的调用位置是baz函数中的bar();位置

clipboard.png

foo函数是在bar函数中调用的,foo函数的调用栈为baz -> bar -> foo,当正在执行的是foo函数时,其前一个调用是bar,所以foo函数的调用位置是bar函数中的foo();位置

clipboard.png

this的绑定规则

找到调用位置后该如何确定this的指向呢?这是有规则可循的,下面我们就来看看这四条规则,了解了规则后,确定this的步骤就变成:找到调用位置,然后判断需要应用四条规则中的哪一条,根据规则得出this的指向。

默认绑定

首先要介绍的是最常用的函数调用类型:独立函数调用。这种调用是直接使用不带任何修饰的函数引用进行调用的,它的调用位置是全局作用域,于是this指向全局对象。可以把这条规则看作是无法应用其他规则时的默认规则。

我们看下面的代码:

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

首先我们要知道一件事,声明在全局作用域中的变量(比如上述代码中的var a = 2)就是全局对象的一个同名属性。它们本质上就是同一个东西,并不是通过复制得到的,就像一个硬币的两面一样。

在代码中,foo()是在全局作用域中直接使用不带任何修饰的函数引用进行调用的,所以foo函数调用时应用this的默认绑定,因此this指向全局对象;既然this指向全局对象,那么this.a便是全局变量a,所以打印的结果为2

注意:严格模式下,禁止this关键字指向全局对象,此时this会绑定到undefined;所以当函数定义在严格模式下或函数内的代码运行在严格模式下时,其中的this绑定的是undefined;特别注意如果仅仅是函数的调用语句运行在严格模式下,那么不受影响,该函数内的this仍然绑定到全局对象

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

foo函数定义在严格模式下,所以this绑定到了`undefined

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

foo函数内部为严格模式,所以this绑定到了undefined

function foo() {
    console.log( this.a );
}
"use strict";
var a = 2;
foo(); // 2

严格模式的标识在foo函数的定义之后,foo函数未定义在严格模式下,仅仅是foo函数的调用语句foo()运行在严格模式下,所以this仍然可以绑定到全局对象

function foo() {
    console.log( this.a );
}
var a = 2;
(function(){
   "use strict";
   foo(); // 2
})()

仅仅是foo函数的调用语句foo()运行在严格模式下,所以this仍然可以绑定到全局对象

温馨提示:通常来说你不应该在代码中混合使用严格模式和n非严格模式。整个程序要么严格要么非严格。然而,有时候你可能会用到第三方库,其严格程度和你的代码有所不同,因此一定要注意这类兼容性细节。

隐式绑定

第二条规则是考虑函数调用位置是否有上下文对象,或者说该函数是否被某个对象“拥有”或者“包含”(仅仅是这么理解一下),如果函数调用位置有上下文对象,那么隐式绑定规则会把该函数中的this绑定到这个上下文对象

function foo() {
    console.log( this.a );
}
var obj = {
    a: 2,
    foo: foo
};
obj.foo(); // 2

首先需要注意的是foo函数的声明方式,及其之后是如何被当作引用属性添加到obj中的。但是无论是直接在obj中定义还是先定义再添加为引用属性,这个函数严格来说都不属于obj对象;然而,调用位置会使用obj上下文来引用函数,因此你可以说函数被调用时obj 对象“拥有”或者“包含”它。

当函数调用位置有上下文对象时,隐式绑定规则会把该函数中的this绑定到这个上下文对象。所以上面的例子中,调用foo()this被绑定到obj,那么this.aobj.a 是一样的,打印的结果便是2

对象属性引用链中只有最顶层或者说最后一层会影响调用位置,看个例子就很容易理解了:

function foo() {
    console.log( this.a );
}
var obj2 = {
    a: 42,
    foo: foo
};
var obj1 = {
    a: 2,
    obj2: obj2
};
obj1.obj2.foo(); // 42

上述对象引用链为 :obj1->obj2,只有最后一层会影响调用位置,也就是只有obj2会影响调用位置,所以foo函数的调用位置的上下文对象为obj2this绑定到obj2

注意:有些情况下会出现隐式丢失,意思就是被隐式绑定的函数丢失绑定对象,也就是说它会应用默认绑定,从而把this绑定到全局对象或者undefined上(取决于是否是严格模式)

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 函数本身,相当于var bar = foo;。因此此时的bar()其实是一个不带任何修饰的函数调用,所以会应用了默认绑定,绑定到全局对象

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"
setTimeout( obj.foo, 100 ); // "oops, global"

参数传递其实就是一种隐式赋值,因此我们传入函数时也会被隐式赋值,所以将obj.foo传递给doFoo函数的参数fn,相当于fn = foo,所以doFoo函数内部的fn()其实是一个不带任何修饰的函数调用,所以会应用了默认绑定,绑定到全局对象

内置函数setTimeout的结果也是一样的。回调函数丢失this绑定是非常常见的,之后我们会介绍如何通过固定this来修复这个问题。

显式绑定

就像我们刚才看到的那样,在分析隐式绑定时,我们必须在一个对象内部包含一个指向函数的属性,并通过这个属性间接引用函数,从而把this 间接(隐式)绑定到这个对象上。那么如果我们不想在对象内部包含函数引用,而想在某个对象上强制调用函数,该怎么做呢?

可以使用函数的call(..)apply(..) 方法。严格来说,JavaScript 的宿主环境有时会提供一些非常特殊的函数,它们并没有这两个方法。但是这样的函数非常罕见,JavaScript 提供的绝大多数函数以及你自己创建的所有函数都可以使用call(..)apply(..) 方法。

这两个方法是如何工作的呢?它们的第一个参数是一个对象,它们会把这个对象绑定到this,接着在调用函数时指定这个this;因为你可以直接指定this的绑定对象,因此我们称之为显式绑定。(如果没有传递第一个参数,也就是没有直接指定this,那么this将绑定到全局对象或者undefined上)

function foo() {
    console.log( this.a );
}
var obj = {
    a:2
};
foo.call( obj ); // 2

通过foo.call(..),我们可以在调用foo时强制把它的this绑定到obj上。

如果你传入了一个原始值(字符串类型、布尔类型或者数字类型)来当作this的绑定对象,这个原始值会被转换成它的对象形式(也就是new String(..)new Boolean(..) 或者new Number(..))。这通常被称为“装箱”。但是在严格模式下this不会被强制转换为一个对象,也就是说传入原始值来当做this的绑定对象,那么它不会转换为对象形式

function foo() {
    console.log( this );
}
foo.call( "cc" ); // String {"cc"}
foo.call( 6 ); // Number {6}
foo.call( true ); // Boolean {true}

"use strict"
function foo() {
    console.log( this );
}
foo.call( "cc" ); // cc
foo.call( 6 ); // 6
foo.call( true ); // true

可惜,显式绑定仍然无法解决我们之前提出的丢失绑定问题

function foo() {
    console.log( this.a );
}
var obj = {
    a:6
};
var bar = function() {
    foo();
};
bar.call(obj); // undefined

可以看出,虽然bar通过call(..)方法显示绑定到了obj,但是其内部的foo()仍然是一个不带任何修饰的函数调用,this绑定到全局对象

硬绑定

显式绑定的一个变种可以解决这个丢失绑定问题,我们称这个变种为硬绑定,下面来看看它是如何解决的:

function foo() {
    console.log( this.a );
}
var obj = {
    a:6
};
var bar = function() {
    foo.call( obj );
};
bar(); // 6
setTimeout( bar, 100 ); // 6
// 硬绑定的bar 不可能再修改它的this
bar.call( window ); // 6

我们创建了函数bar,并在它的内部手动调用了foo.call(obj),因此强制把foothis 绑定到了obj,无论之后如何调用函数barthis始终绑定到obj

一般来说,可以创建一个可重复使用的硬绑定辅助函数:

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函数就可以将foo函数的this始终绑定为obj,由于硬绑定是一种非常常用的模式,所以在ES5中提供了内置的方法Function.prototype.bind,将上面的例子改成该方法的形式,代码如下:

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

bind(..)会返回一个硬编码的新函数,调用这个新函数时会把原始函数的this绑定到传入bind(..)的参数上并调用原始函数,所以foo.bind( obj )会返回一个新函数,然后被赋值给bar,调用bar时会把foo中的this绑定到obj,并且调用foo函数。

API调用的“上下文”

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

比如说:

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"

array.forEach(function(currentValue, index, arr), thisValue)方法用于调用数组的每个元素,并将元素传递给回调函数,它的第二个参数thisValue就可以指定回调函数中的this(如果这个参数为空,那么this将绑定到全局对象或者undefined上);forEach内部实际上就是通过call(..) 或者apply(..) 实现了显式绑定

其他函数还有array.maparray.filterarray.everyarray.some

new绑定

最后一条this的绑定规则,在讲解它之前我们首先需要澄清一个非常常见的关于JavaScript 中函数和对象的误解。

在传统的面向类的语言中,“构造函数”是类中的一些特殊方法,使用new初始化类时会调用类中的构造函数。通常的形式是这样的:

    something = new MyClass(..);

JavaScript也有一个new操作符,使用方法看起来也和那些面向类的语言一样,但是,JavaScriptnew的机制实际上和面向类的语言完全不同

首先我们重新定义一下JavaScript中的“构造函数”:在JavaScript中,构造函数只是一些使用new操作符时被调用的函数,它们并不会属于某个类,也不会实例化一个类。实际上,它们甚至都不能说是一种特殊的函数类型,它们只是被new操作符调用的普通函数而已。(ES6中的Class只是语法糖而已)

自定义函数和内置对象函数(比如Number(..))都可以用new来调用,这种函数调用被称为构造函数调用。这里有一个重要但是非常细微的区别:实际上并不存在所谓的“构造函数”,只有对于函数的“构造调用”

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

  • 创建(或者说构造)一个全新的对象
  • 这个新对象会被执行[[Prototype]]链接([[Prototype]]指向构造函数的原型对象
  • 这个新对象会绑定到该构造函数中的this
  • 执行构造函数中的代码
  • 如果该构造函数没有返回其他对象,那么会自动返回这个新对象

上述过程中的this绑定就被称为new绑定,下面看个简单的例子:

function foo(a) {
    this.a = a;
}
var bar = new foo(2);
console.log( bar.a ); // 2

使用new来调用foo(..)时,我们会构造一个新对象(赋值给了变量bar)并把它绑定到foo函数中的this上,foo函数中的this绑定的就是对象bar

绑定规则的优先级

在了解了四种绑定规则后,我们需要了解一下他们之间的优先级,因为有时候会出现符合多种规则的情况。

毫无疑问,默认绑定的优先级是四条规则中最低的,所以我们可以先不考虑它。

隐式绑定和显式绑定哪个优先级更高?我们来测试一下:

function foo() {
    console.log( this.a );
}
var obj1 = {
    a: 2,
    foo: foo
};
var obj2 = {
    a: 3,
    foo: foo
};
obj1.foo(); // 2
obj2.foo(); // 3
obj1.foo.call( obj2 ); // 3
obj2.foo.call( obj1 ); // 2

可以明显看出,显示绑定优先于隐式绑定,也就是说在判断时应当先考虑是否可以应用显式绑定

那么隐式绑定和new绑定哪个优先级更高?我们也来测试一下:

function foo(something) {
    this.a = something;
}
var obj1 = {
    foo: foo
};
var obj2 = {};

obj1.foo( 2 );//隐式绑定
console.log( obj1.a ); // 2

var bar = new obj1.foo( 4 );//new绑定,相当于vra bar = new foo(4);
console.log( obj1.a ); // 2
console.log( bar.a ); // 4

可以看到new绑定比隐式绑定优先级高,那么现在还需要知道new绑定和显式绑定谁的优先级更高,由于newcall/apply 无法一起使用,因此无法通过new foo.call(obj1) 来直接进行测试,而硬绑定是显示绑定的一种,所以我们使用硬绑定来测试它俩的优先级:

在看代码之前先回忆一下硬绑定是如何工作的。Function.prototype.bind(..) 会创建一个新的包装函数,这个函数会忽略它当前的this绑定(无论绑定的对象是什么),并把我们提供的对象绑定到this上。

这样看起来硬绑定(也是显式绑定的一种)似乎比new 绑定的优先级更高,应该无法使用new来控制this绑定,那实际上是如何的呢?来让代码揭晓答案:

function foo(something) {
    this.a = something;
}
var obj1 = {};

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

var baz = new bar(3);
console.log( obj1.a ); // 2
console.log( baz.a ); // 3

bar函数中的的this被硬绑定到obj1上,但是new bar(3)并没有像我们前面预计的那样把obj1.a修改为3,这说明使用new来调用bar()的时候,bar函数中的this绑定的不是obj1(否则obj1.a应该被修改为3),所以使用new仍然可以控制this绑定,实际上此时bar函数中的this绑定的是一个新对象,这个新对象最后赋值给了baz,所以baz.a的值为3

为什么与预想的不同?因为ES5 中内置的Function.prototype.bind(..)方法的内部会进行判断,会判断硬绑定函数是否是被new调用,如果是的话就会使用新创建的this替换硬绑定的this

所以new绑定的优先级高于显示绑定。

Function.prototype.bind(thisArg[, arg1[, arg2[, ...]]])方法,可以传入参数序列,当绑定函数被调用时,这些参数将置于实参之前传递给被绑定的函数,所以bind(..)的功能之一就是可以把除了第一个参数(第一个参数用于绑定this)之外的其他参数都传给下层的函数(这种技术称为“部分应用”,是“柯里化”的一种)。

正是由于bind(...)的这一功能,如果我们在new中使用硬绑定函数,那么就可以预先设置函数的一些参数,这样在使用new进行初始化时就可以只传入其余的参数,这也就是为什么有些时候会在new中使用硬绑定函数的原因,看个例子:

function foo(p1,p2) {
    this.val = p1 + p2;
}
// 之所以使用null 是因为在本例中我们并不关心硬绑定的this是什么
// 反正使用new的时候this会被修改
var bar = foo.bind( null, "p1" );//传入预先设置的参数p1
var baz = new bar( "p2" );//只需传入剩余的参数p2
baz.val; // p1p2

优先级总结

综上所述,优先级如下:
new绑定 > 显示绑定 > 隐式绑定 > 默认绑定

那么我们在确定this的时候就可以根据下面的步骤来:

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

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

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

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

    var bar = foo()

对于正常的函数调用来说,理解了这些知识就可以明白this的绑定原理了,不过……凡事总有例外!!!

绑定的特殊情况

在某些场景下this的绑定行为会出乎意料,你认为应当应用其他绑定规则时,实际上应用的可能是默认绑定规则。

被忽略的this

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

function foo() {
    console.log( this.a );
}
var a = 2;
foo.call( null ); // 2

那么什么情况下你会传入null呢?一种非常常见的做法是使用apply(..)来“展开”一个数组,并当作参数传入一个函数(ES6中可以直接使用...操作符)。类似地,bind(..)可以对参数进行柯里化(预先设置一些参数),这种方法有时非常有用:

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

这两种方法都需要传入一个参数当作this的绑定对象。如果函数并不关心this的话,你仍然需要传入一个占位值,这时null可能是一个不错的选择,就像代码所示的那样。

然而,总是使用null来忽略this绑定可能产生一些副作用。如果某个函数确实使用了this(比如第三方库中的一个函数),那默认绑定规则会把this绑定到全局对象(在浏览器中这个对象是window),这将导致不可预计的后果(比如修改全局对象。显而易见,这种方式可能会导致许多难以分析和追踪的bug

一种“更安全”的做法是传入一个特殊的对象,把this绑定到这个对象不会对你的程序产生任何副作用。就像网络(以及军队)一样,我们可以创建一个DMZdemilitarized zone,非军事区)对象,如果我们在忽略this绑定时总是传入一个DMZ对象,那就什么都不用担心了,因为任何对于this的使用都会被限制在这个空对象中,不会对全局对象产生任何影响。

由于这个DMZ对象完全是一个空对象,可以使用一个特殊的变量名来表示它,比如ø(这是数学中表示空集合符号的小写形式)。在JavaScript中创建一个空对象最简单的方法都是Object.create(null)Object.create(null){}很像,但是并不会创建Object.prototype这个委托,所以它比{}“更空”,所以之前的例子可以改为:

function foo(a,b) {
    console.log( "a:" + a + ", b:" + b );
}
// 我们的DMZ 空对象
var ø = Object.create( null );
// 把数组展开成参数
foo.apply( ø, [2, 3] ); // a:2, b:3
// 使用bind(..) 进行柯里化
var bar = foo.bind( ø, 2 );
bar( 3 ); // a:2, b:3

间接引用

另一个需要注意的是,你有可能(有意或者无意地)创建一个函数的“间接引用”,在这种情况下,调用这个函数会应用默认绑定规则。

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

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函数的引用,因此调用位置是foo()而不是p.foo()或者o.foo()。根据我们之前说过的,这里会应用默认绑定。

软绑定

之前我们已经看到过,硬绑定这种方式可以把this强制绑定到指定的对象(除了使用new时),防止函数调用应用默认绑定规则。问题在于,硬绑定会大大降低函数的灵活性,使用硬绑定之后就无法使用隐式绑定或者显式绑定来修改this

如果可以给默认绑定指定一个全局对象和undefined以外的值,那就可以实现和硬绑定相同的效果,同时保留隐式绑定或者显式绑定修改this的能力。

可以通过一种被称为软绑定的方法来实现我们想要的效果:

if (!Function.prototype.softBind) {
    Function.prototype.softBind = function(obj) {
        var fn = this;//这个this是指调用softBind的函数
        // 捕获所有 curried 参数(柯里化参数)
        var curried = [].slice.call( arguments, 1 );//arguments指传入softBind的参数列表
        var bound = function() {
            return fn.apply(
                (!this || this === (window || global)) ? obj : this,
                curried.concat.apply( curried, arguments)
            );//这里的this是指调用bound时的this,arguments指传入bound的参数列表
        };
        bound.prototype = Object.create( fn.prototype );
        return bound;
    };
}

除了软绑定之外,softBind(..) 的其他原理和ES5内置的bind(..) 类似。它会对指定的函数进行封装,首先检查调用时的this,如果this绑定到全局对象或者undefined,那就把指定的默认对象obj绑定到this,否则不会修改this。此外,这段代码还支持可选的柯里化(详情请查看之前和bind(..)相关的介绍),看看软绑定的实例:

function foo() {
    console.log("name: " + this.name);
}
var obj = { name: "obj" },
obj2 = { name: "obj2" },
obj3 = { name: "obj3" };

var fooOBJ = foo.softBind( obj );
fooOBJ(); // name: obj

obj2.foo = foo.softBind(obj);
obj2.foo(); // name: obj2 <---- 看!!!通过上下文对象(隐式绑定)绑定到obj2

fooOBJ.call( obj3 ); // name: obj3 <---- 看!通过显示绑定绑定到obj3

setTimeout( obj2.foo, 10 );// name: obj <---- 应用了软绑定,this本来绑定到全局对象,通过软绑定绑定到了obj

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

特殊的箭头函数

我们之前介绍的四条规则已经可以包含所有正常的函数。但是ES6中介绍了一种无法使用这些规则的特殊函数类型:箭头函数。

箭头函数并不是使用function关键字定义的,而是使用被称为“胖箭头”的操作符=>定义的。箭头函数不使用this的四种标准规则,而是根据外层(函数或者全局)作用域来决定this

我们来看看箭头函数的词法作用域:

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 !

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

箭头函数最常用于回调函数中,例如事件处理器或者定时器:

function foo() {
    setTimeout(() => {
        // 这里的this 在词法上继承自foo()
        console.log( this.a );
    },100);
}
var obj = {
    a:2
};
foo.call( obj ); // 2

箭头函数可以像bind(..)一样确保函数的this被绑定到指定对象,此外,其重要性还体现在它用更常见的词法作用域取代了传统的this机制。实际上,在ES6之前我们就已经在使用一种几乎和箭头函数完全一样的模式:

function foo() {
    var self = this; // lexical capture of this
    setTimeout( function(){
        console.log( self.a );
    }, 100 );
}
var obj = {
    a: 2
};
foo.call( obj ); // 2

是不是非常熟悉?

虽然self = this和箭头函数看起来都可以取代bind(..),但是从本质上来说,它们想替代的是this机制,如果你经常编写this风格的代码,但是绝大部分时候都会使用self = this或者箭头函数来否定this机制,那你或许应当:

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

当然,包含这两种代码风格的程序可以正常运行,但是在同一个函数或者同一个程序中混合使用这两种风格通常会使代码更难维护,并且可能也会更难编写。

疑问解答

先来说一下最前面的一个例子的真实情况:

function foo(num) {
    console.log( "foo: " + num );
    // 记录foo 被调用的次数
    this.count++;
}
foo.count = 0;
var i;
for (i=0; i<10; i++) {
    if (i > 5) {
        foo( i );
    }
}
// foo: 6
// foo: 7
// foo: 8
// foo: 9
// foo 被调用了多少次?
console.log( foo.count ); // 0 -- WTF?

先说结论,this指向全局对象,而this.countNaN
通过分析我们可以知道foo的调用位置是全局作用域,然后foo处于非严格模式,所以this指向全局对象,由于this.count的值一开始为undefined,然后进行this.count++;的操作,所以变成NaN

记得前面提到过下面这段话:

严格模式下,禁止this关键字指向全局对象,此时this会绑定到undefined;所以当函数定义在严格模式下或函数内的代码运行在严格模式下时,其中的this绑定的是undefined;特别注意如果仅仅是函数的调用语句运行在严格模式下,那么不受影响,该函数内的this仍然绑定到全局对象

但是测试的时候遇到一种情况一开始让我匪夷所思:

function foo(){
    "use strict";
    console.log(this);
}

setTimeout(foo,100);//Window

foo的函数体处于严格模式下,为什么this还是绑定到全局对象Window?于是我又测试了几种情况:

"use strict";
function foo(){
    console.log(this);
}
setTimeout(foo,100);//Window

//---------分割线-----------

function foo(){
    console.log(this);
}
setTimeout(function(){
    "use strict";
    foo();
},100);//Window

//---------分割线-----------

function foo(){
    "use strict";
    console.log(this);
}
setTimeout(function(){
    foo();
},100);//undefined

只有最后一种情况this绑定到undefined,其他情况仍然绑定到Window
MDN-Window.setTimeout-关于this的问题中,找到一段备注:

备注:在严格模式下,setTimeout( )的回调函数里面的this仍然默认指向window对象, 并不是undefined

但是这个仅仅是告诉了我们结论,并没有给出为什么。经过思考,我给出我自己的猜想,也不知道对不对:

我们知道setTimout是挂在Window下的方法,所以调用时实际上是Window.setTimout,是通过Window对象调用的,一般认为setTimout的伪代码是下面这样:

function setTimeout(fn,delay) {
    // 等待delay 毫秒
    fn(); 
}

但是通过前文的介绍,我们知道

直接使用不带任何修饰的函数引用进行调用的,它的调用位置是全局作用域,非严格模式下绑定到全局对象,严格模式下绑定到undefined

根据setTimeout这种伪代码,等待delay毫秒后,fn()就是一个不带任何修饰的函数调用,而下面的测试确仍然指向全局对象Window

function foo(){
    "use strict";
    console.log(this);
}

Window.setTimeout(foo,100);//Window

所以我猜想,setTimout的伪代码是下面这样:

function setTimeout(fn,delay) {
    // 等待delay 毫秒
    //直接执行fn内的代码,而不是调用fn(相当于把fn中的代码粘贴到此处) 
}

基于这种猜想,我们来看前面的测试代码:

function foo(){
    "use strict";
    console.log(this);
}

Window.setTimeout(foo,100);//Window

相当于下面这样:

Window = {
    setTimeout: function(){
        // 等待100毫秒
        "use strict";
        console.log(this);
    }
}

这样一看,this自然就是指向Window;再看其他三个测试代码:

"use strict";
function foo(){
    console.log(this);
}
setTimeout(foo,100);//Window

//相当于
"use strict";
Window = {
    setTimeout: function(){
        // 等待100毫秒
        console.log(this);
    }
}//通过Window调用setTimeout,this指向Window


//---------分割线-----------

function foo(){
    console.log(this);
}
setTimeout(function(){
    "use strict";
    foo();
},100);//Window

//相当于
Window = {
    setTimeout: function(){
        // 等待100毫秒
        "use strict";
        foo();
    }//通过Window调用setTimeout,其内部调用了foo,而且仅仅是foo的调用处于严格模式,所以foo中的this指向Window
}

//---------分割线-----------

function foo(){
    "use strict";
    console.log(this);
}
setTimeout(function(){
    foo();
},100);//undefined

//相当于
Window = {
    setTimeout: function(){
        // 等待100毫秒
        foo();
    }//通过Window调用setTimeout,其内部调用了foo,但是foo的函数体处于严格模式,所以foo中的this指向undefined
}

似乎一切也说的过去,不过我暂时没有找到权威性的资料来证实,自己先这样理解一下,如果不对,还请大家指正!

尾声

以前对this真是不清不楚,这次彻底的顺了一遍之后清晰多了,每天进步一点点,加油~


Cshine
169 声望5 粉丝

前端魔法修炼ing