this
指向多变,不少隐蔽的 bug 都缘于它。与此同时,this
强大灵活,若是能熟练驾驭,就会写出更简洁、优雅的代码。javascript
社区上对于 this
的讲解虽然很多,但缺少统一梳理。css
this
相关知识点以下:java
曾经在面试阿里某重点部门时,面试官从多个角度考察过我对 this
的理解:全局环境下的this
、箭头函数的 this
、构造函数的 this
、this
的显隐性和优先级,等等。尽管我能一一做答,但是最后的问题:请用一句话总结this
的指向,注意只用一句话。 我却犯难了。面试
有一种普遍流传的说法是:浏览器
谁调用它,
this
就指向谁。app
也就是说,this
的指向是在调用时肯定的。这么说没有太大的问题,但是并不全面。面试官要求我用更加规范的语言进行总结,那么他到底在等什么样的回答呢?函数
咱们还要回到 JavaScript 中一个最基本的概念分析——执行上下文。ui
事实上,调用函数会建立新的属于函数自身的执行上下文。执行上下文的调用建立阶段会决定 this
的指向。到此,咱们能够得出的一个结论:this
this
的指向,是在调用函数时根据执行上下文所动态肯定的。spa
具体环节和规则,能够先“死记硬背”如下几条规律,后面再慢慢一一分析:
this
绑定到 undefined
,不然绑定到全局对象 window
/global
;new
调用,绑定到新建立的对象上;call
/apply
/bind
方法显式调用,绑定到指定参数的对象上;this
决定 this
指向。固然,真实环境多样,咱们来逐一梳理。
这种状况相对简单直接,函数在浏览器全局环境中被简单调用,非严格模式下this
指向 window
;在 use strict
指明严格模式的状况下就是undefined
。咱们来看例题,请描述打印结果:
function f1 () { console.log(this) } function f2 () { 'use strict' console.log(this) } f1() // window f2() // undefined
这样的题目比较基础,可是须要候选人格外注意其变种,请再看一道题目:
const foo = { bar: 10, fn: function() { console.log(this) console.log(this.bar) } } var fn1 = foo.fn fn1()
这里 this
仍然指向的是 window
。虽然 fn
函数在 foo
对象中做为方法被引用,可是在赋值给 fn1
以后,fn1
的执行仍然是在 window
的全局环境中。所以输出 window
和 undefined
,它们至关于:
console.log(window) console.log(window.bar)
仍是上面这道题目,若是调用改变为:
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
指向的就是上一级的对象;不然指向全局环境。
如上结论,面对下题时咱们便再也不困惑:
const student = { name: 'Lucas', fn: function() { return this } } console.log(student.fn() === student)
最终结果将会返回 true
。
当存在更复杂的调用关系时,请看例题:
const person = { name: 'Lucas', brother: { name: 'Mike', fn: function() { return this.name } } } console.log(person.brother.fn())
在这种嵌套的关系中,this
指向最后调用它的对象,所以输出将会是:Mike
。
到此,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
没有问题。难点在第二个和第三个上面,关键仍是看调用 this
的那个函数。console
的 o2.fn()
,最终仍是调用 o1.fn()
,所以答案仍然是 o1
。var fn = o1.fn
赋值以后,是“裸奔”调用,所以这里的 this
指向 window
,答案固然是 undefined
。若是面试者回答顺利,能够紧接着追问,若是咱们须要让:
console.log(o2.fn())
输出 o2
,该怎么作?
通常开发者可能会想到使用 bind/call/apply
来对 this
的指向进行干预,这确实是一种思路。可是我接着问,若是不能使用 bind/call/apply
,有别的方法吗?
这样能够考察候选人基础掌握的深度以及随机应变的思惟能力。答案为:
const o1 = { text: 'o1', fn: function() { return this.text } } const o2 = { text: 'o2', fn: o1.fn } console.log(o2.fn())
仍是应用那个重要的结论:this
指向最后调用它的对象,在 fn
执行时,挂到o2
对象上便可,咱们提早进行了赋值操做。
上文提到 bind/call/apply,在这个概念上,比较常见的基础考察点是:bind/call/apply 三个方法的区别。
这样的问题相对基础,咱们直接上答案:一句话总结,他们都是用来改变相关函数 this
指向的,可是 call/apply
是直接进行相关函数调用;bind
不会执行相关函数,而是返回一个新的函数,这个新的函数已经自动绑定了新的 this
指向,开发者须要手动调用便可。再具体的 call/apply
之间的区别主要体如今参数设定上,这里再也不展开。
用代码来总结:
const target = {} fn.call(target, 'arg1', 'arg2')
至关于:
const target = {} fn.apply(target, ['arg1', 'arg2'])
至关于:
const target = {} fn.bind(target, 'arg1', 'arg2')()
具体基础用法这里再也不科普,若是读者尚不清楚,须要本身补充一下知识点。
咱们来看一道例题分析:
const foo = { name: 'lucas', logName: function() { console.log(this.name) } } const bar = { name: 'mike' } console.log(foo.logName.call(bar))
将会输出 mike
,这不难理解。可是对 call/apply/bind 的高级考察每每会结合构造函数以及组合式实现继承。实现继承的话题,咱们会单独讲到。构造函数的使用案例,咱们结合接下来的例题组合进行分析。
这方面最直接的例题为:
function Foo() { this.bar = "Lucas" } const instance = new Foo() console.log(instance.bar)
答案将会输出 Lucas
。可是这样的场景每每伴随着下一个问题:new
操做符调用构造函数,具体作了什么?如下供参考:
this
指向这个新对象;以上过程,也能够用代码表述:
var obj = {} obj.__proto__ = Foo.prototype Foo.call(obj)
须要指出的是,若是在构造函数中出现了显式 return
的状况,那么须要注意分为两种场景:
function Foo(){ this.user = "Lucas" const o = {} return o } const instance = new Foo() console.log(instance.user)
将会输出 undefined
,此时 instance
是返回的空对象 o
。
function Foo(){ this.user = "Lucas" return 1 } const instance = new Foo() console.log(instance.user)
将会输出 Lucas
,也就是说此时 instance
是返回的目标对象实例 this
。
结论:若是构造函数中显式返回一个值,且返回的是一个对象,那么 this
就指向这个返回的对象;若是返回的不是一个对象,那么 this
仍然指向实例。
首先咱们再来温习一下相关结论。
结论:箭头函数使用 this
不适用以上标准规则,而是根据外层(函数或者全局)上下文来决定。
来看题目:
const foo = { fn: function () { setTimeout(function() { console.log(this) }) } } console.log(foo.fn())
这道题中,this
出如今 setTimeout()
中的匿名函数里,所以 this
指向window
对象。若是须要 this
指向 foo
这个 object 对象,能够巧用箭头函数解决:
const foo = { fn: function () { setTimeout(() => { console.log(this) }) } } console.log(foo.fn()) // {fn: ƒ}
单纯箭头函数中的 this
很是简单,可是综合全部状况,结合 this
的优先级考察,这时候 this
指向并很差肯定。请继续阅读。
咱们经常把经过 call
、apply
、bind
、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)
输出分别为 二、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
值为 2。即通过 bar(2)
执行后,obj1
对象为:{a: 2}
。
当再使用 bar
做为构造函数时:
var baz = new bar(3) console.log(baz.a)
将会输出 3。咱们看 bar
函数自己是经过 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) console.log(bar.call(obj2))
将会输出 2。因为 foo()
的 this
绑定到 obj1
,bar
(引用箭头函数)的this
也会绑定到 obj1
,箭头函数的绑定没法被修改。
若是将 foo
彻底写成箭头函数的形式:
var a = 123 const foo = () => a => { console.log(this.a) } const obj1 = { a: 2 } const obj2 = { a: 3 } var bar = foo.call(obj1) console.log(bar.call(obj2))
将会输出 123
。
这里我再“抖个机灵”,仅仅将上述代码的第一处变量 a
的赋值改成:
const a = 123 const foo = () => a => { console.log(this.a) } const obj1 = { a: 2 } const obj2 = { a: 3 } var bar = foo.call(obj1) console.log(bar.call(obj2))
答案将会输出为 undefined
,缘由是由于使用 const
声明的变量不会挂载到window
全局对象当中。所以 this
指向 window
时,天然也找不到 a
变量了。
事实上,this
的指向涉及的规范繁多,优先级也较为混乱。刻意刁难并非很好的面试作法,一些细节候选人若是没有记住也不是太大的问题。做为面试官,出一些开放性题目。
其中,最典型的一道题目为:实现一个 bind
函数。
做为面试者,我也曾经在头条的面试流程中被问到模拟 bind
。这道题并不新鲜,部分读者也会有本身的解答思路,并且社区上关于原生 bind
的研究也不少。可是,咱们这里想强调的是,可能有一些细节被你们忽略了。在回答时,我每每先实现一个初级版本,而后根据 ES5-shim 源码进一步说明。
Function.prototype.bind = Function.prototype.bind || function (context) { var me = this; var args = Array.prototype.slice.call(arguments, 1); return function bound () { var innerArgs = Array.prototype.slice.call(arguments); var finalArgs = args.concat(innerArgs); return me.apply(context, finalArgs); } }
这样的实现已经很是不错了。可是,就如同以前 this
优先级分析所示:bind
返回的函数若是做为构造函数,搭配 new
关键字出现的话,咱们的绑定 this
就须要“被忽略”。
为了实现这样的规则,开发者就应该须要考虑如何区分这两种调用方式。具体来说 bound
函数中就要进行 this instanceof
的判断。
另一个细节是,函数具备 length
属性,表示形参的个数。上述实现方式形参的个数显然会失真。咱们的实现就须要对 length
属性进行还原。但是难点在于:函数的 length
属性值是不可重写的。
这样的内容通常属于“超纲”范畴,但在面试中可以很好地体现面试者平时的积累,以及对源码的阅读和思考,显然是加分项。