JavasScript--this绑定的原则可不仅仅是谁调用指向谁

说到this的绑定这绝对是让许多程序员非常头疼的一件事儿,所以经过多年的经验总结出一句被广大程序员认为是标杆的经典:this的指向取决于谁调用,谁调用就取决于谁,否则就指向window.由此可见是函数调用的位置决定了this的指向,这句话看似简单,容易理解,可却并不那么简单,所以还是有必要总结一下this绑定的那些事儿。

this的绑定规则是什么?

(一)默认规则

我们先来看一段代码:

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

通过上面的代码,我们可以到变量a是声明在全局的变量,当在调用bar的时候,发现this的指向是window全局变量,这时this指向的是全局变量。bar() 是直接使用不带任何修饰的函数引用进行调用的,因此只能使用 默认绑定,无法应用其他规则。

注意,这里我们使用的是非严格模式,如果是使用严格模式,那么this将会指向的是undefined,并且在严格模式下,this的指向和函数的调用位置无关。

隐式绑定

我们先来看一段代码:

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

看这段代码,我们声明了一个函数bar,和一个对象obj,他们俩毫无关系,只不过是对象obj有个属性叫bar引用了函数bar,这样以来,在调用到obj.bar()的时候就会使用obj的上下文来引用函数,因此,可以说函数被调用时 obj 对象“拥 有”或者“包含”它。所以当bar被调用的时候,他的落脚点就是指向了obj,当函数引 用有上下文对象时,隐式绑定规则会把函数调用中的 this 绑定到这个上下文对象。因为调用 bar() 时 this 被绑定到 obj,因此 this.a 和 obj.a 是一样的。

隐式丢失

一个最常见的 this 绑定问题就是被隐式绑定的函数会丢失绑定对象,也就是说它会应用默认绑定,从而把 this 绑定到全局对象或者 undefined 上,取决于是否是严格模式。看下面的代码:

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

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

var a = 'this is window'
var foo = obj.bar
foo() // this is window

可以看到,与上面的代码基本相同,只是多了foo = obj.bar 这一行foo是obj.bar 的一个引用,那么此时foo就是一个不带任何修饰符的函数,那么此时的this就是指向了全局,因此输出的结果是this is window.使用了默认绑定。

这里需要注意的是,回调函数也同样使用该规则

显式绑定

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

是的,你想的没有错,call和apply,在javascript中的绝大多数函数,以及自己创造的函数都可以通过这俩个函数来实现这一功能。它们的第一个参数是一个对象,它们会把这个对象绑定到 this,接着在调用函数时指定这个 this。因为你可以直接指定 this 的绑定对象,因此我们称之为显式绑定

  • 硬绑定
    来看下面的代码:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    function bar () {
    console.log(this.a)
    }
    function foo () {
    bar.call(obj)
    }
    var obj = {
    a: 1
    }

    foo() // 1
    setTimeout(foo) // 1
    foo.call(window)
    我们可以看到,在foo函数中将bar函数的this和obj对象绑定在一起。无论之后如何调用函数foo,它总会手动在 obj 上调用bar。这种绑定是一种显式的强制绑定,因此我们称之为硬绑定。而硬绑定的使用场景就:
    (1)创建一个包裹函数,传入所有的参数并返回接收到的所有值:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    function foo (item) {
    console.log(this.a, item)
    return this.a + item
    }
    var obj = {
    a: 2
    }
    var bar = function () {
    foo.apply(obj, arguments)
    }
    var b = bar(3) // 2 3
    console.log(b) // 5
    (2)创建一个 i 可以重复使用的辅助函数:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    function foo(something) {
    console.log( this.a, something );
    return this.a + something;
    }
    // 简单的辅助绑定函数
    function bind(fn, obj) {
    return function() {
    return fn.apply(obj, argements)
    };
    }
    var obj = {
    a:2
    }
    var bar = bind( foo, obj );
    var b = bar( 3 ); // 2 3
    console.log( b ); // 5
    注意,由于硬绑定是非常常用的方式,所以ES5提供了内置方法来实现:Function.prototype.bind(),返回一个硬编码的新函数,它会把参数设置为 this 的上下文并调用原始函数。
  • API调用的‘上下文’
    确保你的回调 函数使用指定的 this。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    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
    这些函数实际上就是通过 call(..) 或者 apply(..) 实现了显式绑定,这样你可以少些一些 代码。

    new 绑定

    new操作符对我们来说,再熟悉不过了,所以这里说的new绑定主要就说,在执行new操作符,调用构造函数的时候,绑定的this。
  1. 创建(或者说构造)一个全新的对象。
  2. 这个新对象会被执行[[原型]]连接。
  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

使用 new 来调用 foo(..) 时,我们会构造一个新对象并把它绑定到 foo(..) 调用中的 this 上。new 是最后一种可以影响函数调用时 this 绑定行为的方法,我们称之为 new 绑定。

this绑定优先级的判断

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

this绑定的例外

万物并非全部都在三界之内,有四种猴子却是在这三届之外,那么this绑定四项规则之外也还是有特例存在的。

被忽略的this

如果把null, undefined传入call,apply, bind作为this绑定的对象,那么这些值是会被忽略的,实际应用的是默认绑定。
一般情况下,通过apply来展开一个数组,并且当作是参数传入到一个函数中,同样,bind对参数进行颗粒化的时候,也会有同样的操作:

1
2
3
4
5
6
function foo (a, b) {
console.log('a:' + a, 'b:' + b)
}
foo.apply(null, [1, 2])
var bar = foo.bind(null, 2)
bar(3) // a:2, b:3

我们可以看到,其实函数并不关心this的指向问题,所以,我们需要传入一个null作为站位符。当然了传入null,也是会有副作用的。如果使用了this,那么此操作就将this绑定到了window.

更安全的this

不知道你还记得吗,又一个这样的方法,Object.create(null),这个方法就是创建了一个空对象,这个空要比’{}’更为纯粹,因为他连prototype都没有。
所以,在call,apply,bind中用var obj = Object.create(null)的obj来做占位符将不会有任何的副作用,那么this就会更加的安全。

间接引用

1
2
3
4
5
6
7
8
9
10
11
12
13
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 绑定对象的并不是调用位置是否处于严格模式,而是函数体是否处于严格模式。如果函数体处于严格模式,this会被绑定到 undefined,否则 this会被绑定到全局对象。

软绑定

软绑定肯定是相对于硬绑定而言,硬绑定这种方式可以把 this 强制绑定到指定的对象,这导致绑定this的灵活性大大降低。如果可以给默认绑定指定一个全局对象和 undefined 以外的值,那就可以实现和硬绑定相 同的效果,同时保留隐式绑定或者显式绑定修改 this 的能力。那么就是我们说的软绑定。

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
27
28
29
30
31
32
33
34
35

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;
};
}
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 <---- 看!!!
fooOBJ.call( obj3 ); // name: obj3 <---- 看!
setTimeout( obj2.foo, 10 );
// name: obj <---- 应用了软绑定

与内置的bind方法相似,首先检查调用时的 this,如果 this 绑定到全局对象或者 undefined,那就把 指定的默认对象 obj 绑定到 this,否则不会修改 this。

箭头函数的this词法

ES6中出现的箭头函数,又是一个奇葩,它对于this的判断对于上面的规则并不是适用。在箭头函数中this的指向问题应该是根据外层(函数或者全局)作用域来决定this。

1
2
3
4
5
6
7
8
9
10
11
12
13
function foo () {
return (a) => {
console.log(this.a)
}
}
var obj1 = {
a: 2
}
var obj2 = {
a: 3
}
var bar = foo.call(obj1)
foo.call(obj2) // 2并不是3

所以,我们可以看到,foo的内部返回的函数中捕获的是foo的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
24
var a = 1
function foo () {
setTimeout(() => {
console.log(this.a)
})
}
foo() // 1

var b = 2
function bar () {
setTimeout(function () {
console.log(this.b)
})
}
bar() // undefined

var c = 3
function fun () {
var self = this
setTimeout(function () {
console.log(self.c)
})
}
fun() // 3

看上面的三段代码,区别就在于setTimeout的回调函数。

  • 第一段回调中如果是箭头函数,那么箭头函数中this的指向取决于外层函数,很明显外层函数的this指向window,所以结果是1
  • 第二段回调函数是一个匿名函数,所以this的只想取决于他自己,所以他的this指向的是函数本身
  • 通过var self = this, this的指向取决与fun函数,所以结果是3,self取代了this的机制

所以,一般情况下,我们都会使用self = this 这样的方式来否定this机制,那么,

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

(完)

  • 版权声明: 本博客所有文章除特别声明外,著作权归作者所有。转载请注明出处!
  • Copyrights © 2015-2022 Lee
  • 访问人数: | 浏览次数:

请我喝杯咖啡吧~

支付宝
微信