关于this
指向的具体细节和规则后面再慢慢分析,这里可以先“死记硬背”一下几条规律:
this
会被绑定到undefined
上,再非严格模式下则会被绑定到全局对象window
上。new
方法调用构造函数时,构造函数内的this
会被绑定到新创建的对象上。call
/apply
/bind
方法显式调用函数时,函数体内的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
指向就是上一级对象;否则指向全局环境。
参考以上结论,运行下面代码,最终将会返回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
对象的方法被调用。
与之相关的常见的考察点是: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
的高级考察往往需要面试者结合构造函数及组合来实现继承。
先上代码,带着问题去思考:
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
出现在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: ƒ}
&emsp:单纯的箭头函数中的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
。