此文首发于 https://lijing0906.github.io
在上篇JS继承中涉及到的好几个知识点都想写,比如call()
、apply()
从而牵出和bind()
的区别,由Object.create()
想到与new Object()
、{}
的区别,原型链以及作用域,然而在参考其他博客时发现,应该先把JS中的this
指向弄明白。
《你不知道的JavaScript》(上卷)第二部分讲到了this
,算是比较权威的关于this
的讲解,但我觉得有些地方讲得还是晦涩难懂,需要结合一些博客来理解会容易理解一些。
this
提供一种优雅的方式来隐式“传递”一个对象引用,在函数中显示传入一个上下文对象,避免在代码越来越复杂的情况下造成上下文对象混乱。
this
就像它的词性一样,是个代词,表指代什么,在JS中表示指代某个对象。this
是在函数运行时绑定到某个对象上,并不是在函数定义时被绑定的,因此this
的绑定(即this
的指向)与函数的声明位置没关系,只取决于函数的调用方式。
说五种绑定规则之前,先说说不同作用域中this
的指向,包括全局作用域(Global Scope)和局部作用域(Local Scope)。
全局作用域(Global Scope)
所有运行环境中JS运行时都只有唯一的全局对象,在浏览器中,全局对象是window
;在node.js
中全局对象是global
。
在全局作用域中(任何函数体外的代码),this
指向的是全局对象,不管是不是在严格模式下。
局部作用域(Local Scope)
局部作用域可以理解为{}
包裹的区域,this
的指向就根据调用方法不同而不同。
this
指向全局对象window
。function foo() {
console.log(this);
console.log(this.a);
}
var a = 2;
foo(); // Window对象 2
this
会绑定到undefined
上。this
的绑定规则完全取决于调用位置,但是只有foo()
***运行***在非严格模式下时,默认绑定才能把this
绑定到全局对象;严格模式下***调用***函数则不影响默认绑定。function foo() { // 运行在严格模式下,this会绑定到undefined
"use strict";
console.log(this.a);
}
var a = 2;
// 这里虽然foo()是在全局作用域中执行,但是foo里的代码运行在严格模式下,所以this被绑定到了undefined上。
foo(); // TypeError: Cannot read property 'a' of undefined
// --------------------------------------
function foo() {
console.log(this.a);
}
var a = 2;
(function() { // 严格模式下调用函数则不影响默认绑定
"use strict";
foo(); // 2
})();
当函数作为对象属性被调用时,函数中的this
指向(被绑定到)调用这个函数的对象,这就是隐式绑定。注意:最后一层在调用中起作用,即最接近函数调用的那层起作用。
function foo() {
console.log(this.a);
}
var a = 2;
var obj = {
a: 3,
foo: foo
};
obj.foo(); // 3
为什么是3?那就需要了解一下内存中基础类型数据和引用数据类型是怎么存储的,可以看看阮一峰的JavaScript的this原理。
上面代码的执行过程:获取obj.foo
属性——>根据引用关系(引用地址)找到foo
函数,执行函数调用。obj
离foo()
最近,this
被绑定到obj
上。
function foo() {
console.log(this.a);
}
var a = 2;
var obj1 = {
a: 4,
foo: foo
};
var obj2 = {
a: 3,
obj1: obj1
};
obj2.obj1.foo(); // 4
同样看一下调用过程:获取obj2.obj1
属性——>根据引用关系(引用地址)获取obj1
对象——>再重复第二步找到foo
函数——>执行函数调用。obj1
离foo()
最近,this
被绑定到obj1
上。
2. 隐式丢失(函数别名)
function foo() {
console.log(this.a);
}
var a = 2;
var obj = {
a: 3,
foo: foo
};
var bar = obj.foo; // 把foo()的引用地址赋值给bar
bar(); // 2
为什么没有隐式绑定到obj
上?因为obj.foo
是引用类型的属性,它其实是一个指向foo()
函数的引用地址[打印一下obj.foo就能发现,打印出来的是foo函数],因此bar
得到的是foo()
函数的引用地址,调用bar()
时this
被绑定到全局对象window
上了。
3. 隐式丢失(回调函数)
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"
// ----------------------------------------
// JS环境中内置的setTimeout()函数实现和下面的伪代码类似:
function setTimeout(fn, delay) {
// 等待delay毫秒
fn(); // <-- 调用位置!
}
道理同函数别名的this绑定丢失一样。
通过call()
或apply()
方法。第一个参数是一个对象,在调用函数时将这个对象绑定到this
上。因为直接指定this
的绑定对象,称之为显示绑定。
function foo() {
console.log(this.a);
}
var a = 2;
var obj1 = {
a: 3,
};
var obj2 = {
a: 4,
};
foo.call(obj1); // 3 强制把this绑定到obj1上
foo.call(obj2); // 4 强制把this绑定到obj2上
显示绑定无法解决丢失绑定问题。
解决方案:
function foo() {
console.log(this.a);
}
var obj = {
a: 2
};
var bar = function() {
foo.call(obj);
};
bar(); // 2 函数别名
setTimeout(bar, 100); // 2 回调函数
// 硬绑定的bar不可能再修改它的this
bar.call(window); // 2
典型应用场景是创建一个包裹函数,负责接收参数并返回值。
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) {
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
ES5内置了Function.prototype.bind
,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
context
),其作用和bind()
一样,确保回调函数使用指定的this
。这些函数实际上通过call()
和apply()
实现了显式绑定。function foo(el) {
console.log(el, this.id);
}
var obj = {
id: "awesome"
}
var myArray = [1, 2, 3]
// 调用foo()时把this绑定到obj
myArray.forEach(foo, obj);
// 1 awesome 2 awesome 3 awesome
function Func() {};
var func = new Func();
看看new
操作符具体做了什么就明白new
绑定了。
var obj = new Object();
obj.__proto__ = Func.prototype;
Func
中的this
指向obj
,并执行Func
的函数体。var result = Func.call(obj);
obj
。如果是引用类型,就返回这个引用类型的对象。if (typeof(result) == "object"){
func = result;
} else {
func = obj;;
}
箭头函数无法使用上述四条规则,而是根据外层(函数或者全局)作用域(词法作用域)来决定this。
// 普通函数
function foo1(){
console.log(this.a);
}
// 箭头函数
var foo2 = () => {
console.log(this.a);
}
var a = 2;
var obj1 = {
a: 3,
foo: foo1
};
var obj2 = {
a: 3,
foo: foo2
};
// 调用普通函数
obj1.foo(); // 3
// 调用箭头函数
obj2.foo(); // 2 根据外层作用域,obj2.foo指向的foo2函数的外层是全局对象,因此输出2
foo2.call(obj2); // 2 ,箭头函数中显示绑定不会生效
// 回调函数是普通函数
function foo1(){
return function(){
console.log(this.a);
}
}
// 回调函数是箭头函数
function foo2(){
// 此处返回的箭头函数的外层是foo2被调用的作用域,foo2将在obj2.foo()时被调用,此时的外层作用域时obj2
return () => {
console.log(this.a);
}
}
var a = 2;
var obj1 = {
a: 3,
foo: foo1
};
var obj2 = {
a: 3,
foo: foo2
};
// 执行普通回调
var bar1 = obj1.foo();
bar1(); // 2
// 执行箭头回调
var bar2 = obj2.foo();
bar2(); // 3
优先级按照下面的顺序来进行判断:
new
中调用(new
绑定)?如果是的话this
绑定的是新创建的对象。call
、apply
(显式绑定)或者硬绑定调用?如果是的话,this
绑定的是指定的对象。this
绑定的是那个上下文对象。undefined
,否则绑定到全局对象。在显示绑定中,把null
或者undefined
作为this
的绑定对象传入call
、apply
或者bind
,这些值在调用时会被忽略,实际应用的是默认规则。
function foo() {
console.log(this.a);
}
var a = 2;
foo.call(null); // 2
foo.call(undefined); // 2
其实这不符合预期要求,如果某个函数确实使用了this
,那会使用默认绑定把this
绑定到全局对象上去。
安全的做法:传入一个特殊的对象(空对象),把this
绑定到这个对象上,这样才不会对你的程序产生任何副作用。
JS中创建一个空对象最简单的方法是Object.create(null)
,这个和{}
很像,但是并不会创建Object.prototype
这个委托,所以比{}
更空。
function foo(a, b) {
console.log( "a:" + a + ",b:" + b );
}
// 创建一个空对象
var ø = Object.create(null);
// 把数组”展开“成参数
foo.apply(ø, [2, 3]); // a:2,b:3
// 使用bind()进行柯里化
var bar = foo.bind(ø, 2);
bar(3); // a:2,b:3
// p.foo = o.foo的返回值是目标函数的引用,所以调用位置是foo()而不是p.foo()或者o.foo()
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
强制绑定到指定的对象(new
除外),防止函数调用时使用默认绑定规则。但是会降低函数的灵活性,使用硬绑定之后就无法使用隐式绑定或者显式绑定来修改this
。undefined
以外的值,那就可以实现和硬绑定相同的效果,同时保留隐式绑定或者显示绑定修改this
的能力。// 默认绑定规则,优先级排最后
// 如果this绑定到全局对象或者undefined,那就把指定的默认对象obj绑定到this,否则不会修改this
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;
};
}
使用:软绑定版本的foo()
可以手动将this
绑定到obj2
或者obj3
上,但如果应用默认绑定,则会将this
绑定到obj
。
function foo() {
console.log("name:" + this.name);
}
var obj = { name: "obj" },
obj2 = { name: "obj2" },
obj3 = { name: "obj3" };
// 默认绑定,应用软绑定,软绑定把this绑定到默认对象obj
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
我们在使用js的过程中,对于this
的理解往往觉得比较困难,再调试过程中有时也会出现一些不符合预期的现象。很多时候,我们都是通过一些变通的方式(如:使用具体对象替换this
)来规避的问题。