一文打尽this,对执行上下文说Yes
this到底指向谁
关于this
指向的具体细节和规则后面再慢慢分析,这里可以先“死记硬背”一下几条规律:
- 在函数体中,非显式或隐式地简单调用函数时,再严格模式下,函数内的
this
会被绑定到undefined
上,再非严格模式下则会被绑定到全局对象window
上。 - 一般使用
new
方法调用构造函数时,构造函数内的this
会被绑定到新创建的对象上。 - 一般通过
call
/apply
/bind
方法显式调用函数时,函数体内的this
会被绑定到指定参数的对象上。 - 一般通过上下文调用函数时,函数体内的
this
会被绑定到该对象上。 - 在箭头函数中,
this
的指向时由外层(函数或全局)作用域来决定的。
实战例题分析
全局环境中的this
function f1() {
console.log(this);
}
function f2 () {
'use strict'
console.log(this);
}
f1(); //Window
f2(); //undefined
函数在浏览器全局环境中被简单调用,在非严格模式下this
指向window
,在通过'use strict'
指明严格模式的情况下this
指向undefined
const foo = {
bar: 10,
fn: function () {
console.log(this);
console.log(this.bar);
}
}
var fn1 = foo.fn;
fn1(); //Window undefined
这里的this
还是指向window
。虽然fn
函数在foo
对象中用来作为对象的方法,但是在赋值给fn1
之后,fn1
仍然是在window
的全局环境中执行的。
将上面的代码调用改为一下形势:
const foo = {
bar: 10,
fn: function () {
console.log(this);
console.log(this.bar);
}
}
foo.fn(); //{bar: 10, fn: ƒ} 10
这时this
指向的是最后调用它的对象,在foo.fn()
语句中,this
指向foo
对象。请记住,在执行函数时不考虑显式绑定,如果函数中的this
是被上一级的对象调用的,那么this
指向就是上一级对象;否则指向全局环境。
上下文对象调用中的this
参考以上结论,运行下面代码,最终将会返回true
const student = {
name:'mgd',
fn: function () {
return this;
}
}
console.log(student.fn() === student); //true
当存在更复杂的调用关系时 ,如以下代码中的嵌套关系,this
会指向最后调用它的对象,代码输出是mgd
const person = {
name: 'lpb',
brother: {
name: 'mgd',
fn: function () {
return this.name;
}
}
}
console.log(person.brother.fn()); //mgd
至此,this
的上下文对象调用已经介绍的比较清楚了。再看一道高阶题目:
const o1 = {
text: 'o1',
fn: function () {
return this.text;
}
}
const o2 = {
text: 'o2',
fn: function () {
return o1.fn();
}
}
const o3 = {
text: 'o3',
fn: function () {
var fn = o1.fn;
return fn();
}
}
console.log(o1.fn());
console.log(o2.fn());
console.log(o3.fn());
答案是o1
、o1
、undefined
,你猜对了吗?下面来分析一下:
- 第一个
console
输出o1
很简单,难点在第二个和第三个console
上,关键还是看调用this
的那个函数 - 第二个
console
中的o2.fn()
最终调用的还是o1.fn()
,所以结果为o1
- 第三个
console
中的o3.fn()
通过var fn = o1.fn;
的赋值进行了“裸奔”调用,这里的this
指向window
,运行结果是undefined
假如你在面试时已经能回答到这些了,面试官可能会追问:需要让第二个console
打印o2
怎么做?
如果你回答bind
、call
、apply
来对this
进行干预,面试官接着就会问你如果不用这些方法呢?
回答肯定是有的,这个问题考察的是对基础知识的掌握深度和编程思维,方法如下:
const o1 = {
text: 'o1',
fn: function () {
return this.text;
}
}
const o2 = {
text: 'o2',
fn: o1.fn
}
console.log(o2.fn()) // o2
以上方法同样使用了那个重要的结论, this
指向最后调用它的对象。在上面的代码中,我们提前进行了赋值操作,将函数fn
挂载到o2
对象上,fn
最终作为o2
对象的方法被调用。
通过bind、call、apply改变this指向
与之相关的常见的考察点是:call
/bind
/apply
的区别
这样的问题相对基础,直接上答案:它们都是用来改变相关函数this
指向的,但是call
和apply
是直接进行相关函数的调用的;bind
不会执行相关函数,而是返回一个新的函数,这个新的函数已经自动绑定了新的this
指向,可以手动调用它。如果再说具体一点,就是call
和apply
之间的区别主要体现在参数设定上,详情请阅读红宝书第四版。
下面看一道例题并分析:
const foo = {
name: 'mgd',
logName: function () {
console.log(this.name);
}
}
const bar = {
name: 'lpb'
}
foo.logName.call(bar);
以上代码的执行结果为lpb
,这不难理解。但是对call
/bind
/apply
的高级考察往往需要面试者结合构造函数及组合来实现继承。
构造函数和this
先上代码,带着问题去思考:
function Foo () {
this.bar = 'mgd';
}
const instance = new Foo();
console.log(instance.bar); //mgd
这样的场景往往伴随着一个问题:new
操作符调用构造函数时做了什么呢?以下给出简单回答,仅供参考
- 创建一个对象
- 将构函数的
this
指向这个新的对象 - 为这个对象添加属性、方法等。
- 最终返回新的对象。
上述过程用代码表述:
var obj = {};
obj.__proto__ = Foo.prototype;
Foo.call(obj);
需要指出的是,如果在构造函数中出现显式return
的情况,那么需要注意,其可以细分为两个场景:
- 执行下面代码输出
undefined
,此时instance
返回的是空对象o
function Foo () {
this.user = 'mgd';
const o = {};
return o;
}
const instance = new Foo();
console.log(instance.user); //undefined
- 执行下面代码输出
mgd
,也就是说inatance
此时返回的是目标对象实例:
function Foo () {
this.user = 'mgd';
return 1;
}
const instance = new Foo();
console.log(instance.user); //mgd
所以,如果构造函数中显式返回一个值,且返回的是一个对象(复杂类型),那么this
就指向这个返回的对象;如果返回的不是一个对象(基本类型),那么this
指向实例。
箭头函数中的this
在箭头函数中,this
的指向是由外层(函数或全局)作用域来决定的。
下面这段代码中的,this
出现在setTimeout
中的匿名函数中,因此this
指向window
对象:
const foo = {
fn: function () {
setTimeout(function() {
console.log(this);
});
}
}
foo.fn() //Window
如果需要让this
指向这个对象,则可以使用箭头函数来完成,代码如下:
const foo = {
fn: function () {
setTimeout(() => {
console.log(this);
});
}
}
foo.fn() //{fn: ƒ}
单纯的箭头函数中的this
指向问题非常简单,但是如果综合所有情况,并结合this
的优先级进行考察,那么这时的this
指向并不容易确定
this优先级
通常把call
、bind
、apply
、new
对this
进行绑定的情况称为显式绑定,把根据调用关系确定this
指向的情况称为隐式绑定。
那么显式绑定和隐式绑定谁的优先级更高?下面揭晓:
执行下面的代码:
function foo (a) {
console.log(this.a);
}
const obj1 = {
a: 1,
foo: foo
}
const obj2 = {
a: 2,
foo: foo
}
obj1.foo.call(obj2);
obj2.foo.call(obj1);
输出分别是2、1,也就是说:call
、apply
的显式绑定一般来说优先级更高。 再看下面的代码:
function foo (a) {
this.a = a;
}
const obj1 = {};
var bar = foo.bind(obj1);
bar(2);
console.log(obj1.a);
上述代码通过绑定bind
将bar
函数中的this
绑定为obj1
对象。执行bar(2)
后,obj1.a
值为1,即执行bar(2)
后,obj1
对象为{a:2}
当再使用bar
作为构造函数时,例如下面的代码,则会输出3:
var baz = new bar(3);
console.log(baz.a);
bind
函数本身是通过bind
方法构造的函数,其内部已经将this
绑定为obj1
,当它再次作为构造函数通过new
被调用时,返回的实例就已经与obj1
解绑了。也就是说,new
绑定修改了bind
绑定中的this
指向,因此new
绑定的优先级比显式bind
绑定的更高。
再来看一段代码:
function foo () {
return a => {
console.log(this.a);
}
}
const obj1 = {
a: 2
}
const obj2 = {
a: 3
}
const bar = foo.call(obj1);
bar.call(obj2);
输出结果为2.由于foo
中的this
绑定到了obj1
上,所以bar
(引用箭头函数)中的this
也会绑定到obj1
上,箭头函数的绑定无法被修改。
如果将foo
写成如下箭头函数的形式,则会输出123:
var a = 123;
const foo = () => a => {
console.log(this.a);
}
const obj1 = {
a: 2
}
const obj2 = {
a: 3
}
var bar = foo.call(obj1);
bar.call(obj2); //123
将上面代码中第一处变量a
的声明修改一下,即变成一下这种,猜一猜结果是什么?
const a = 123;
答案为undefined
,原因是const
声明的变量不会挂载到window
全局对象上。因此,this
指向window
时,自然找不到a
变量了。
开放性例题分析
实现一个 bind
函数:
Function.prototype.bind = Function.prototype.bind || function (context) {
let that = this;
let args = Array.prototype.slice.call(arguments, 1);
return function bound () {
let innerArgs = Array.prototype.slice.call(arguments);
let finalArgs = args.concat(innerArgs);
return that.apply(context, finalArgs);
}
}
这样的实践已经非常不错了。但是,就如之前在this
优先级分析那里所展示的规则:bind
返回的函数如果作为构造函数搭配new
关键字出现的话,绑定的this
就会被忽略。
为了实现这样的规则,开发者需要考虑如何区分这两种调用方式。具体来讲就是要在bound
函数中进行this instanceof
判断。
另外一个细节是,函数具有length
属性,用来表示形参的个数。在上述实现方式中,形参的个数显然会失真。所以,改进的实现方式需要对length
属性进行还原。可是难点在于,函数的length
属性值是不可重写的。
总结
我们看到this
的用法纷繁多象,趋势不容易彻底掌握,需要在阅读之外继续消化吸收。只有‘死记’,才能‘活用’。一个优秀的前端工程师,不完全在于回答面试题目的正确率,更在于如何思考问题、解决问题。如果不懂this
指向,那就动手实践一下;如果不了解原理,就翻出规范看一下。与诸君共勉,希望在不久的将来,能彻底掌握this
。