每个函数的this
是在调用的时候被绑定的,完全取决于函数的调用位置
调用位置
在理解this
的绑定过程之前,首先要理解调用位置:调用位置就是函数在代码中被调用的位置(而不是声明位置)。
通常来说,寻找调用位置就是寻找“函数被调用的位置”,但是做起来并没有这么简单,因为某些编程模式可能会隐藏真正的调用位置。
最重要的是分析调用栈(就是为了达到当前执行位置所调用的所有函数)。我们所关心的调用位置就在当前正在执行的函数的前一个调用中。
下面我们来看一下到底什么是调用栈和调用位置:
function baz() {
// 当前调用栈为baz
// 因此当前的调用位置为全局作用域
console.log("baz");
bar();//bar调用位置
}
function bar() {
// 当前调用栈为baz->bar
// 因此当前的调用位置为baz重
console.log("bar");
foo();//foo调用位置
}
function foo() {
// 当前的调用栈为baz->bar->foo
// 因此当前的调用位置在bar中
console.log("foo");
}
baz();//baz调用位置
绑定规则
你必须找到调用位置,然后判断需要应用下面四条规则中的哪一条,然后根据优先级排列
默认绑定
首先要介绍的是最常用的函数调用类型:独立函数调用。可以把这条规则看作是无法应用其他规则时的默认规则
思考下面的代码:
function foo() {
console.log(this.a);//2
}
var a = 2;
foo();
通过上面的例子可以发现,声明在全局作用域中的变量(比如var a = 2)就是全局对象的一个同名属性。它们本质上是一个东西,并不是通过复制得到的
接下来在调用foo()
时,this.a
被解析会全局变量,为什么?因为在本列中,函数调用时应用了this
的默认绑定,因此this
指向全局对象
那么我们怎么知道这里应用了默认绑定呢?可以通过分析调用位置来看看foo()
是如何调用的。在代码中foo()
是直接使用不带任何修饰的函数引用调用的,因此只能使用默认绑定,无法使用其他规则
如果使用严格模式(strict mode
),那么全局对象将无法使用默认绑定,因此this
会绑定到undefined
function foo() {
'use strict';
console.log(this.a);//2
}
var a = 2;
foo();
Uncaught TypeError: Cannot read property 'a' of undefined
at foo (this.js:23)
at this.js:26
这是一个微妙但是非常重要的细节,虽然this
的绑定规则完全取决于调用位置,但是只有foo()
运行在非strict mode
下时,默认绑定才能绑定到全局对象;严格模式下与foo()
的调用无关
function foo() {
console.log(this.a);//2
}
var a = 2;
(function () {
'use strict';
foo();
})();
隐式绑定
function foo() {
console.log(this.a);
}
var obj = {
a:2,
foo:foo
}
obj.foo();//2
首先需要注意的是foo()
的声明方式,及其之后是如何被当做引用属性添加到obj
中,但是无论是直接在obj
中定义还是先定义再添加为引用属性,这个函数严格说来都不属于obj
对象
然而,调用位置会使用obj
上下文来引用函数,因此你可以说函数在被调用时obj
对象“拥有”或者“包含”它。
无论你如何称呼这个模式,当foo()
被调用时,它的落脚点确实指向obj
对象。当函数引用有上下文对象时,隐式绑定规则会把函数调用中的this
对象绑定到这个上下文对象。因为调用foo()
时this
被绑定到obj
,因此this.a
和obj.a
是一样的
对象属性引用链只有最顶层或者最后一层会影响调用位置,如
function foo() {
console.log(this.a);//42
}
var obj2 = {
a:42,
foo:foo
}
var obj1= {
a:2,
obj2:obj2
}
obj1.obj2.foo();//对象属性引用链
上面的例子中函数引用在被调用时,其上下文对象为obj2
,所以隐式绑定将foo()
中的this
值绑定到了obj2
上
隐式丢失
一个最常见的this
绑定问题就是隐式绑定的函数会丢失绑定对象,也就是说它会应用默认绑定,从而把this
绑定到全局作用域或者undefined
上,取决于是否是严格模式
function foo() {
console.log(this.a);//42
}
var obj = {
a:2,
foo:foo
};
var bar = obj.foo;//函数别名
var a = "oops,global";//a是全局对象的属性
bar();//oops,global
由于this
的值取决于函数被调用时的位置,而不是函数被引用或者是被声明时的位置,在上面的列子中 var bar = obj.foo
将foo()
函数的引用传递给了bar
变量,所以调用bar()
就会调用foo()
函数,而其中的this
值取决于bar()
的调用位置,即当前正在执行的函数的前一个调用中
function foo() {
console.log(this.a);
}
var obj = {
a:2,
foo:foo
};
function doFoo(fn) {
fn();
}
var a = "oops,global";//a是全局对象的属性
doFoo(obj.foo);//oops,global
在上面的例子中通过参数传递引用的方式将obj.foo
对foo()
函数的引用传递给了fn
变量,所以调用其值取决于fn()
的调用位置,又由于调用位置即为正在执行的函数的前一个调用,fn()
函数的调用位置为doFoo()
,由于在函数doFoo()
中未能找到相应的属性,所以根据作用域链向上层查找,查找结果为oops,global
function foo() {
console.log(this.a);
}
var obj = {
a:2,
foo:foo
};
function doFoo(fn) {
this.a = 5;
fn();
}
var a = "oops,global";//a是全局对象的属性
doFoo(obj.foo);//5
在上面的例子中首先会在调用位置也就是doFoo()
函数中查找相应属性,找到了该属性,所以打印结果为5
如果把函数传入语言内置的函数而不是传入你自己声明的函数,如下:
function foo() {
console.log(this.a);
}
var obj = {
a:2,
foo:foo
};
var a = "oops,global";//a是全局对象的属性
setTimeout(obj.foo,1000);//oops,global
结果依然是oops,global
除此之外有一种情况的this
的行为会出乎我们的意料:调用回调函数的函数可能会修改this
。在一些流行的JavaScript
库中事件处理器常会把回调函数的this
强制绑定到触发事件的DOM
元素上,解决办法:在函数开始时var self = this
,将this
值赋值出去,后面使用self
显示绑定
在分析隐式绑定时,我们必须在一个对象内部包含一个指向函数的属性,并通过这个属性间接应用函数,从而把this
间接绑定到这个对象上,那么如果我们不想在对象内部包含函数引用,而想在某个对象上强制调用函数,该怎么做呢?
JavaScript
中的“所有”函数都有一些有用的特性,可以来解决这个问题。具体点说,可以使用函数的call(...)
和apply(...)
方法。严格来说,JavaScript
的宿主环境有时会提供一些非常特殊的函数,它们并没有这两个方法。但是这样的函数非常罕见,JavaScript
提供的绝大多数函数以及你自己创建的所有函数都可以使用call()
和apply()
方法
这两个参数时如何工作的呢?它们的第一个参数是一个对象,它们会把这个对象绑定到this
,接着在调用函数时指定这个this
,因此你可以直接指定this
的绑定对象,因此我们称之为显示绑定
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(...)
),这通常被称为”装箱”,还可以通过这个将其变为其他对象形式,如:
// 绑定事件
var sortBtnA = document.querySelectorAll(".sortBig");
sortBtnB = document.querySelectorAll(".sortSmall");
// 为sortBtnA与sortBtnB应用数组遍历方法
// 应用该方法后将sortBtn的NodeList转变为Array;
sortBtnA = Array.prototype.slice.call(sortBtnA);
sortBtnB = Array.prototype.slice.call(sortBtnB);
在上面的例子中sortBtnA,sortBtnB
是返回的一组DOM
节点,虽然有length
等方法,但这组数据并不是数组,所以要应用数组的方法,要先将其转变为数组,所以在上述列子中调用了Array.prototype.slice.call(...)
方法,调用完成后sortBtnA
就变为了数组对象,便可以使用数组对象的属性与方法,注意,在将其转变为数组对象的时候要根据对象不同的方法传入的参数进行选择,如果使用Array.prototype.map.call()
方法就不能转变,因为Array()
的map()
方法的传入参数为Fun
,而本例中传入的参数为字符串,所以选择slice()
方法
可惜,显示绑定仍然无法解决我们之前提出的丢失绑定问题
1. 硬绑定
但是显示绑定的一个变种可以解决这个问题
function foo() {
console.log(this.a);
}
var obj = {
a:2
};
var bar = function () {
foo.call(obj);
};
bar();//2
setTimeout(bar,100);//2
var a = 3;
<!--硬绑定的bar不能再修改它的this-->
bar.call(window);//2
在上面的列子中创建了一个函数bar()
,并且在它的内部手动调用了foo.call(obj)
,因此强制把foo
的this
绑定到了obj
,无论之后如何调用函数bar
,它总会手动在bar
上调用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
另一个使用方法是创建一个i
可以重复使用的辅助函数
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
在上面的例子中bar为
function () {
return fn.apply(obj, arguments);
};
调用bar()
后b
为函数foo()
的返回值也就是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);
console.log(b);//5
API调用的上下文
第三方库中的许多函数,以及JavaScript
语言和宿主环境中的许多新的内置函数,都提供了一个可选的参数,通常被称为”上下文”,其作用和bind()
一样,确保你的回调函数使用指定的this
function foo(el) {
console.log(el,this.id);
}
var obj = {
id:"awesome"
};
[1,2,3].forEach(foo,obj);
1 "awesome"
2 "awesome"
3 "awesome"
这些函数实际上就是通过call(...)
或者apply(...)
实现了显示绑定,这样就可以少些一些代码
new
绑定
在JavaScript
中,构造函数只是一些使用new
操作符时被调用的函数。它们并不会属于某个类,也不会实例化一个雷。实际上,它们甚至都不能说是一种特殊的函数类型,它们只是被new
操作符调用的普通函数而已,包括内置对象函数在内的所有函数都可以用new
来调用,这种函数调用被称为构造函数调用,这里有一个重要但是非常细微的区别:实际上并不存在所谓的”构造函数”,只有对于函数的”构造调用”
使用new
来调用函数,或者说发生构造函数调用时,会自动执行下面的操作。
1. 创建或者说构造一个全新的对象
2. 这个新对象会被执行[[原型]]链接
3. 这个新对象会绑定到函数调用的this
4. 如果函数没有返回其他对象,那么new
表达式中的函数调用会自动返回这个新对象
function foo(a) {
this.a = a;
}
var bar = new foo(2);
console.log(bar.a);
在上面的列子中使用new
来调用foo(...)
时,会构造一个新对象并把它绑定到foo(...)
调用中的this
上
优先级
现在已经了解了函数调用中this
绑定的四条规则,现在需要做的就是找到函数的调用位置并判断应当应用哪条规则,毫无疑问,默认绑定的优先级是四条规则中最低的
可以按照下面的顺序来进行判断:
1. 函数是否在new
中调用(new
绑定),如果是的话this绑定的是新创建的对象
2. 函数是否通过call,apply(显示绑定)或者硬绑定,如果是的话,this绑定的是指定的对象
3. 函数是否在某个上下文对象中调用(隐式绑定),如果是的话,this绑定的是那个上下文对象
4. 如果都不是的话,使用默认绑定。如果在严格模式下,就绑定到undefined,否则绑定到全局对象
全文摘自于:《你不知道的JavaScript》