第2章:this全面解析
2.1 调用位置
- 在理解this的绑定之前,首先要理解“函数调用的位置”,即函数在代码中被调用的位置。只有仔细分析了调用位置才能解释,函数中的this到底引用的是什么?
- 寻找“函数被调用的位置”,其实并没有想象中的简单,因为JS是很灵活的语言,经常将函数也作为参数进行传递,可能会隐藏真正的调用位置。
- 所以我们需要分析函数的调用栈。所谓调用栈,就是为了到达当前执行位置,所调用过的所有函数,所以我们可以把调用栈想象成一个函数调用链,举例说明:
function baz(){
//当前位置是baz
console.log("my name is baz");
//baz中调用bar
bar();
}
function bar(){
//当前位置是bar
console.log("my name is bar");
//bar中调用foo
foo();
}
function foo(){
//当前位置是foo
console.log("my name is foo");
}
//window下调用baz
baz();
/*
* 所以foo的调用栈(链)就是:
* window -> baz -> bar -> foo
* /
2.2 绑定规则
- 通过函数的调用位置,并应用JavaScript中四条决定this绑定的规则,就能分析this的引用值了。
2.2.1 默认绑定
- 首先是最常用的函数调用类型:独立函数调用。所谓独立函数调用,就是没有应用其他规则的默认调用规则。举例:
var a = 2;
function foo(){
console.log(this.a);
}
foo(); //输出 2
- 当调用
foo()
函数时,this.a
指向了全局变量a
,因为在默认绑定下,this
指向全局对象。 - 那如何辨别这里应用的是默认绑定呢?这时候,就需要运用我们前面讲的“分析函数调用位置”了,在这段代码中,
foo()
函数是直接调用的,不带任何修饰,也不被任何函数包含,所以可以确定是默认绑定。
注意:如果 函数内使用严格模式(strict mode) ,是不能将全局对象用于默认绑定的,最终
this
会绑定到undefined
上。举例说明:var a = 2; function foo(){ "use strict"; console.log(this.a); } foo(); //输出 TypeError : this is undefined
但 在严格模式下调用函数 ,则不影响默认绑定。举例说明:
var a = 2; function foo(){ console.log(this.a); } (function(){ "use strict"; foo(): // 输出2 )();
由于我们可能会使用众多第三方库,所以代码中可能会混合使用strict模式和非strict模式,因此一定要注意这类的兼容性问题。
2.2.2 隐式绑定
- 第二条规则,就是通过函数调用位置,函数是否属于某个对象的属性。
var obj = {
a : 2,
foo : foo
}
function foo(){
console.log(this.a);
}
obj.foo(); // 输出 2
- 你看
foo()
方法的声明方式,它是被当做引用属性添加到了obj
对象中,这种情况下,obj
对象拥有/包含了foo()
方法。 - 当函数有包含自己的对象时,隐式绑定规则会把
this
绑定到这个对象。 - 因此,调用
foo()
时,this
被绑定到了obj
对象,在函数中this.a
和obj.a
的引用是一样的。 - 值得注意的是,如果是多层嵌套对象下的函数,就只在最后一层中起作用。举例:
function foo(){
console.log(this.a);
}
var obj1 = {
a : 2,
obj2 : {
a : 42,
foo : foo
}
}
obj1.obj2.foo(); //输出 42
- 值得注意的是,如果将
obj1.obj2.foo
函数的引用赋值给另一个变量,然后以默认绑定的方式调用函数,不管是自定义的函数,还是JS的内置函数,则还是会应用默认绑定规则:
var a = 'oops,global';
var bar = obj1.obj2.foo;
function runFoo(){
obj1.obj2.foo();
}
// 都是输出 'oops,global'
bar();
runfoo();
setTimeout(obj1.obj2.foo,100);
2.2.3 显示绑定
- 如果不想在对象内部包含函数引用,想在某个对象上强制调用函数,该怎么做呢?
- JavaScript中的函数都有一些特性,可以用来解决这个问题。比如函数的
call()
和applay()
方法 - 这两个方法,传入的第一个参数是一个对象,就是留给
this
准备的,调用时会将其绑定到this
。因为可以直接指定this
的绑定对象,因此我们称之为显示绑定。
function foo(){
console.log(this.a);
}
var obj = {
a : 2
};
foo.call(obj); // 输出 2
- 但如果传入的参数是原始值(字符串、布尔或者数值类型)当做
this
的绑定对象的话,这个原始值会被转换成它的对象形式。也就是new String()
、new Boolean()
或者new Number()
,这个过程通常叫做装箱。 - 1. 硬绑定
function foo(){
console.log(this.a);
}
var obj = {
a : 2
};
var bar = function(){
foo.call(obj);
};
bar(); // 2
setTimeout(bar,100);
//硬绑定不能再修改它的this
bar.call(window); // 2
- 函数
bar()
在它内部手动调用了foo.call(obj)
,强制把foo
的this
绑定到了obj
。无论之后如何调用函数bar
,总会手动在obj
上调用foo
。这种显式的强制绑定,称之为硬绑定。 - 硬绑定的典型应用场景就是创建一个包裹函数,负责接收参数并返回值:
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
- 另一种方式就是创建一个可以重复使用的辅助函数:
function foo(something){
cosnole.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
:
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
的上下文,并调用原始函数。 - 2. API调用的“上下文”
- 许多函数都提供了一个可选的参数,其作用和
bind()
函数一样,确保你的回调函数使用指定的this
。举例:
function foo(el){
console.log(el,this.id);
}
var obj = {
id : 'awesome'
}
[1,2,3].forEach(foo,obj);
- 通过
call()
和apply()
实现显示绑定,可以少写代码。
2.2.4 new绑定
- 在讲解最后一条
this
的绑定规则之前,首先要澄清一个常见的关于JavaScript中函数和对象的误解。 - 在传统的面向类的语言中,“构造函数”是类中的一些特殊方法,使用new初始化类时会调用类的构造函数,
something = new MyClass()
。然而,JavaScript中的new的机制实际上和面向类的语言完全不同。 - 我们重新定义一些JavaScript中的“构造函数”:在JavaScript中,构造函数只是使用new操作符时被调用的函数,它们并不属于某一个对象,也不会实例化一个类。
- 所有函数都可以用new来调用,这种函数调用被称为构造函数调用。实际上,并不存在所谓的“构造函数”,只有对函数的“构造调用”。
- 使用new来调用函数,会自动执行下面的操作:
- 创建一个全新的对象
- 这个新对象会被执行[[Prototype]]连接
- 这个新对象会绑定到函数调用的this
- 如果函数没有返回其他对象,那么new表达式的函数调用会自动返回这个新对象。
function foo(a){
this.a = a;
}
var bar = new foo(2);
console.log(bar.a); // 2
- 使用new来调用
foo()
时,我们会构造一个新对象,把它绑定到foo()
调用中的this上,我们称之为new绑定。
2.3 优先级
- 上文通过大篇幅讲了函数调用中,this绑定的四条规则:默认绑定、隐式绑定、显示绑定和new绑定。但如果调用应用了多条规则就必须给这些规则设定优先级了。
- 毫无疑问,默认绑定的优先级是最低的,暂不考虑它。
- 隐式绑定和显示绑定哪一个优先级更高?我们来测试一下:
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); // 3
- 可以看到,显式绑定优先级更高。
- 接下来,我们要测试,new绑定和隐式绑定的优先级谁高谁低:
function foo(something){
this.a = something;
}
var obj1 = {
foo : foo
}
var obj2 = {}
obj1.foo(2);
console.log(obj1.a); //2
obj1.foo.call(obj2,3);
console.log(obj2.a); //3
var bar = new obj1.foo(4);
console.log(obj1.a); //2
console.log(bar.a); //2
- 可以看到new绑定比隐式绑定的优先级更高,
obj1.a
的值一直没改变。 - 那new绑定和显示绑定,谁的优先级更高呢?(由于new和call/apply无法一起使用,所以通过硬绑定来测试)
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
被硬绑定到了obj1
上,但new baz(3)
并没有把obj1.a
修改为3. - 话说回来,之所以在new中使用硬绑定函数,主要目的是想预先设置一些参数,这样在使用new进行初始化时就可以传入其他参数了。举例:
function foo(p1,p2){
this.val = p1 + p2;
}
var bar = foo.bind(null,"p1");
var baz = new bar("p2");
baz.val; //p1p2
- 根据优先级就能判断函数调用时应用的是哪条规则了,判断的步骤:
- 函数是否进行了new调用,如果是的话,this绑定的是新创建的对象;
- 函数是否通过
call
、apply
或者硬绑定调用,如果有的话,this绑定的是指定的对象; - 函数是否在某个对象中调用,如果是的话,this绑定的是该对象;
- 如果都不是的话,使用默认绑定,绑定到全局对象。(严格模式下,绑定到undefined)
2.4 绑定例外
2.4.1 被忽略的this
- 把null或者undefined作为this传入call、apply或者bind,这些值在函数调用时会被忽略,应用默认的绑定规则:
function foo(){
console.log(this.a);
}
var a = 2;
foo.call(null); // 2
- 什么场景下会传入null呢?比如使用apply来遍历输出一个数组,或者通过
bind()
进行柯里化:
function foo(a,b){
console.log('a:'+a+'b:'+b);
}
foo.apply(null,[2,3]); // a:2,b:3
//先预先传入参数a
var bar = foo.bind(null,2);
//调用时再传入参数b
bar(3); // a:2,b:3
- 但这种方式可能会导致许多难以分析和追踪的bug,我们可以用更安全的方式。
- 更安全的做法就是不传入null,而是传入一个空的对象,把this绑定到这个对象,就好像创建一个非军事区的隔离对象一样,以确保不会对你的程序产生任何副作用。
function foo(a,b){
console.log('a:'+a+'b:'+b);
}
var o = Object.create(null);
foo.apply(o,[2,3]); // a:2,b:3
//先预先传入参数a
var bar = foo.bind(o,2);
//调用时再传入参数b
bar(3); // a:2,b:3
- 我们通过
Object.create(null)
来创建对象,它和直接以字面量{}
创建对象很相似,但前者不会创建Object.prototype
的委托,所以它比{}
更空。
2.4.2 间接引用
- 另一个需要注意的是,可能会有意无意的创建一个函数的“间接引用”,而在这种情况下,调用这个函数会应用默认绑定规则。间接引用最容易在赋值时发生:
function foo(){
console.log(this.a);
}
var a = 2;
var o = {
a : 3,
foo : foo
}
var p = {
a : 4
}
o.foo(); // 3
(function(){
p.foo = o.foo
})(); // 2
2.4.3 软绑定
- 硬绑定可以把this强制绑定到指定的对象,以防止函数调用应用默认绑定规则。问题在于,硬绑定会大大降低函数的灵活性,硬绑定之后,就无法使用隐式绑定或者显示绑定来修改this。
if(!Function.prototype.softbind){
Function.prototype.softbind = function(obj){
var fn = this;
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
2.5 this词法
- 前面解说的四条规则几乎也包含所有函数,但ES6中介绍了一种无法使用这些规则的特殊函数类型:箭头函数。
- 箭头函数使用操作符
=>
来定义,箭头函数不适用this的四种标准规则,而是根据外层作用域来决定this。
function foo(){
return (a) => {
console.log(this.a);
}
}
var obj1 = {
a : 2
}
var obj2 = {
a : 3
}
var bar = foo.call(obj1);
bar.call(obj2); // 2 , 不是 3 !
- 箭头函数最常用于回调函数中,例如事件处理器或者定时器:
function foo(){
setTimeout(() => {
console.log(this.a);
},100);
}
var obj = {
a : 2
};
foo.call(obj); // 2
2.6 小结
- 如果要判断一个运用中函数的this绑定,需要找到函数的调用位置,然后按顺序应用四条规则来判断this的绑定对象:
- 由new调用?绑定到新创建的对象;
- 由call或者apply/bind调用?绑定到指定的对象;
- 由对象调用?绑定到那个对象;
- 默认:严格模式下绑定到undefined,否则绑定到全局对象;
- 有些调用无意中使用默认绑定规则。如果想“更安全” 地忽略this绑定,可以使用一个空的临时对象,比如
o = Object.create(null)
,以保护全局对象。 - ES6中的箭头函数并不会使用四条标准的绑定规则,而是根据当前的词法作用域来决定this。