JavaScript基础系列--战胜this

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

消除误解--this指向自身

因为this的英文释义,许多人都会将其理解成指向函数自身(JavaScript 中的全部函数都
是对象),可是实际上this并不像咱们所想的那样指向函数自身,咱们能够经过下面的栗子验证一下~segmentfault

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基础系列---执行环境与做用域链app

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

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

寻找调用位置

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

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

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真是不清不楚,此次完全的顺了一遍以后清晰多了,天天进步一点点,加油~

相关文章
相关标签/搜索