前言
本篇文章,我们来总结归纳下万恶的this
以及衍生出来的call/apply/bind
对this进行绑定,想了很久,决定用实例演示的方式来讲解this,这样才能够理解this,因为this确实变化莫测,只靠概念,是不能够理解的;之后如果有看到更好的案例,也会同步更新到文章中。
总目录单击链接进行跳转:👉前端面试之路(目录)
this的绑定方式
首先我们要牢记于心this的指向,是在函数被调用的时候确定的,因此,this的指向便非常灵活,情况多样,常见的this一共有5种绑定方式:
- 默认绑定(非严格模式下this指向全局对象, 严格模式下
this
会绑定到undefined
) - 隐式绑定(当函数(
fn
)引用有上下文对象的时候, 如obj.fn()
的调用方式,fn
内部的this
是指向obj
) - 显式绑定(通过
call()
或者apply()
方法直接指定this
的绑定对象) - new操作符(执行过程中会将新生成的对象绑定到函数调用的
this
) - 箭头函数绑定(
this
的指向由外层作用域环境决定的)
那让我们开始找到相应的题目,进行分析。
1.默认绑定
我们先来看默认绑定: 非严格模式下,this
指向window
,严格模式下this
指向undefined
,这句话其实有些歧义和令人不解的地方,我们通过几个题目来分析一下。
题目一:
var a = 1;
function fn () {
console.log(this.a)
}
fn();
我们知道,如果用var
来声明变量(不在函数内)的话,会自动挂载到全局,全局调用函数,相当于是window
调用了这个函数,所以上边这段代码相当于:
window.a = 1
function fn () {
console.log(this.a)
}
window.fn()
答案显而易见,输出为:
1
而如果我们把var
声明改为let
或const
,那么结果是什么呢?可以试一下,没错,是undefined
,因为let
和const
声明的变量,不会被挂在到window
对象中。
题目二:
"use strict";
var a = 1;
function fn () {
console.log('inner-this', this)
console.log(window.a)
console.log(this.a)
}
console.log(window.fn)
console.log('outer-this', this)
fn();
当我们在最上边写上use strict
的时候,相当于开启了严格模式,但所谓的严格模式,只是将fn
函数中的this
指向了undefined
,二并不会影响到全局的this
指向,所以inner-this
打印的是undefined
,outer-this
打印的是window
对象,全局下使用var
声明的变量a
,依旧会被挂在到全局。
所以输出结果为:
2.隐式绑定
这种就是面试中经常出现的类型,那么对于判断这种this
的指向,我们只需要记住哪个对象最后调用函数,函数中的this
就指向那个对象(箭头函数除外)。 我们来看题目:
题目一:
function fn () {
console.log(this.a)
}
var obj = { a: 1, fn }
var a = 2
obj.fn()
我们利用上边的那句口诀,经过分析,发现最后调用fn
的,是obj
对象,所以,fn
函数内部的this
,便指向obj
对象,答案显而易见输出:
1
还有两种隐式绑定的情况,非常具有迷惑性,很容易绕晕,那就是将函数赋给一个新的变量,或者将函数作为参数,进行传递,我们还是通过几道题目来分析。
题目一:
function fn () {
console.log(this.a)
};
var obj = { a: 1, fn };
var a = 2;
var fn2 = obj.fn;
var obj2 = { a: 3, fn2 }
obj.fn();
fn2();
obj2.fn2();
先来判断obj.fn()
的输出,没错,和上一题一样,很容易就能分析出此时this
指向obj
,输出1;而fn2
被赋值成了obj.fn
,在调用的时候,出现了隐式丢失,依旧是window
来调用的,所以此时this
指向window
,输出2;再来分析obj2.fn2()
,此时fn2
是被obj2
所调用了,又出现了隐式丢失,所以this
指向obj2
,输出3,最终结果为:
1
2
3
题目二:
function foo () {
console.log(this.a)
}
function fooWrapper (fn) {
console.log(this)
fn()
}
var obj = { a: 1, foo }
var a = 2
var obj2 = { a: 3, fooWrapper }
obj2.fooWrapper(obj.foo)
我们来分析一下,这道题中,fooWrapper
函数是被obj2
对象所调用的,所以我们可以先得出fooWrapper
中的this
是指向obj2
对象的,所以先打印出{ a: 3, fooWrapper: f }其实就是obj2这个对象
;而obj.foo
被当做参数传递到了fooWrapper
中,此时是window
调用的fn
,所以,此时会输出2,最终结果为:
{ a: 3, fooWrapper: f } // 其实就是obj2这个对象
2
所以我们可以得出结论在函数被当成参数传递进另一个函数时,会发生隐式丢失的问题,其this
并不会受外层包裹它的函数所影响,非严格模式下指向window
,严格模式下指向undefined
,现在再看这句话,是不是比最开始看到,明白了很多了呢?同样的代码,可以在顶部加上use strict
查看结果,来验证这句话。
3.显示绑定
就是使用一些方法,强行绑定函数内部this
的指向,比如call apply bind
,需要先注意下他们之间的区别:
- 经由
call()
和apply()
绑定的函数,会直接调用执行; call()
和apply()
用法几乎相同,第一个参数都为绑定的对象,如果第一个参数为null
或者undefined
的话,会自动忽略这个参数;他们之间的区别就在于之后的传参方式:call
接收多个参数,apply
接收一个数组;bind()
绑定会生成一个新的函数,需要手动再次调用,才会执行;
我们先通过一个比较简单的题目来看下他们之间的区别,顺便温习一下call apply bind
的基本用法:
题目一:
function fn (x, y) {
console.log(this.a)
console.log(x + y)
}
var obj = { a: 1 }
var a = 2
fn(5, 6)
fn.call(obj, 5, 6)
fn.apply(obj, [5, 6])
fn.bind(obj)(5, 6)
首先fn
被调用的时候,因为是window
调用的fn
,所以此时fn
中的this
指向window
,从而打印1, 11;接下来的3种显示绑定,fn
中的this
便指向了obj
,然后立即调用执行(bind
因为返回的是一个函数,所以还需要手动加括号进行调用),结果都输出1,11;我们可以发现区别如上所述,就是传参方式不同。
题目二:
var obj1 = {
a: 1
}
var obj2 = {
a: 2,
fn1: function () {
console.log(this.a)
},
fn2: function () {
setTimeout(function () {
console.log(this)
console.log(this.a)
}.call(obj1), 0)
}
}
var a = 3
obj2.fn1()
obj2.fn2()
首先可以分析出obj2.fn1
中,因为是ob2
调用的fn1
,所以fn1
中的this
指向obj2
,首先输出2;再来看obj2.fn2
,因为函数作为setTimeout
的参数传入发生了隐式丢失,所以函数内部的this
按理来说应该是指向window
的,但是我们使用了call方法,改变了this
指向为obj1
,所以输出{ a: 1 } 1
,最后输出结果为:
2
{ a: 1 } // 就是obj1对象自己
1
题目三:
再来看一道返回匿名函数发生隐式丢失,与显示绑定结合的题目吧。
var obj = {
a: 'obj',
fn: function () {
console.log('fn:', this.a)
return function () {
console.log('inner-a:', this.a)
}
}
}
var a = 'outer-a'
var obj2 = { a: 'obj2' }
obj.fn()()
obj.fn.call(obj2)()
obj.fn().call(obj2)
看起来花里胡哨,我们只要仔细阅读,就不难分析出结果:
- 首先看
obj.fn()()
,注意了,为啥是两个括号呢?其实是执行了2个操作,首先执行了obj.fn()
函数,输出结果为fn: obj
,之后,obj.fn()
返回了一个匿名函数后,又执行了这个匿名函数,因为是window
调用的,所以此时又会输出inner-a: outer-a
。 - 之后再来看
obj.fn.call(obj2)()
,比较长,我们慢慢来看,首先obj.fn
注意此时没加括号,说明obj.fn
没有被调用,可以理解为找到了这个fn
,之后使用call
方法,给obj.fn
进行显示绑定到obj2
对象,所以,此时obj.fn
中的this
是指向obj2
的,首先输出了fn: obj2
,之后,又出现了一个括号,和上边一样,也是匿名函数此刻被调用了,依旧是window
对齐进行的调用,所以此时又会输出inner-a: outer-a
。 - 最后再来看
obj.fn().call(obj2)
,会发现好理解了许多,区别也很明显,call
是为obj.fn()
执行后,返回的匿名函数,进行了显示绑定,所以输出结果为fn: obj
,之后又会输出inner-a: obj2
。
4.new操作符进行绑定
在普通函数被当成构造函数,执行new
操作生成对象时,函数中的this
会被绑定为新创建的对象。
题目一:
function Person (age) {
this.age = age
this.fn1 = function () {
console.log(this.age)
}
this.fn2 = function () {
return function () {
console.log(this.age)
}
}
}
var person1 = new Person(20)
person1.fn1()
person1.fn2()()
我们可以分析到person1
对象创建后person1.fn1()
打印出来的就是构造函数中传入的20,所以会先输出20;而person1.fn2()
返回的是一个匿名函数,之后匿名函数又被调用,调用者是window
,而window.age
没有被定义,所以会输出undefined
。
其实和用字面量创建对象十分类似,使用
new
关键字创建对象的this
指向几乎是没有区别,可以靠同一套逻辑来进行判断。
5.箭头函数绑定
之前我们说过,哪个对象最后调用函数,函数中的this
就指向那个对象(箭头函数除外),为啥箭头函数除外呢?因为箭头函数中的this
,是要根据作用域链进行查找,来决定的。
题目一:
var obj = {
name: 'obj',
fn1: () => {
console.log(this.name)
},
fn2: function () {
console.log(this.name)
return () => {
console.log(this.name)
}
}
}
var name = 'window'
obj.fn1()
obj.fn2()()
先来分析obj.fn1()
,因为fn1
是一个箭头函数,所以在调用的时候,其外部作用域是window
,所以先输出window
;再看obj.fn2()()
,因为obj.fn2
是个普通函数,所以执行时,先打印的this.name
为obj
,之后返回了一个匿名箭头函数,它的this
指向是由外部作用域确定的,所以它内部的this
其实用的就是fn2
中的this
,所以打印的this.name
依旧是obj
,最终输出为:
'window'
'obj'
'obj'
题目二:
如果将普通函数和箭头函数嵌套,那么this
该如何判断呢?根据排列组合我们可以得到4种情况:普通套普通;普通套箭头;箭头套普通;箭头套箭头,你别说,写项目的时候,还真会有人这样写,然后就会遇到很奇怪的bug。
var name = 'window'
var obj1 = {
name: 'obj1',
fn: function () {
console.log(this.name)
return function () {
console.log(this.name)
}
}
}
var obj2 = {
name: 'obj2',
fn: function () {
console.log(this.name)
return () => {
console.log(this.name)
}
}
}
var obj3 = {
name: 'obj3',
fn: () => {
console.log(this.name)
return function () {
console.log(this.name)
}
}
}
var obj4 = {
name: 'obj4',
fn: () => {
console.log(this.name)
return () => {
console.log(this.name)
}
}
}
obj1.fn()()
obj2.fn()()
obj3.fn()()
obj4.fn()()
我们一条一条来分析:
obj1.fn()
在执行后,因为fn
是一个普通函数,所以内部的this
指向obj
,首先输出obj1
,之后返回了一个匿名的普通函数,和前文说过的一样,是window
来调用的,所以又会输出window
,我们已经很熟悉了;obj2.fn()
在执行后,结果是obj2
,因为返回的匿名函数是箭头函数,所以其内部的this
使用的就是fn
中的this
,输出obj2
;obj3.fn()
是一个箭头函数,由外部作用域决定this
取值,所以先输出window
,返回的匿名普通函数,调用者是window
,所以输出window
;obj4.fn()()
同理,是两个箭头函数,所以输出结果为两个window
;
我们还可以得出一个结论,箭头函数中的
this
由外层作用域决定,并且指向函数定义时的this
,而并非调用时。同时,我们虽然没法用call apply bind
来改变箭头函数中的this
指向,但是我们可以通过改变箭头函数外层作用域的this
指向,间接的改变箭头函数中this
的指向。
关于this
的内容,我们暂时告一段落,接下来,我们写一下之前使用过new call apply bind
的原理,面试中的高频考点哦~
new的实现原理
//写一个模拟new的函数
function mockNew() {
//获取第一个参数即构造函数,因为shift的返回值就是第一个参数,同时arguments数组中第一个参数被移除掉了
let Constructor = [].shift.call(arguments);
//创建一个新对象
let obj = {};
//新对象的__proto__指向构造函数的prototype,从而obj能方位原型上的属性
obj.__proto__ = Constructor.prototype;
// 上边两个步骤可以合并为let obj = Object.create(Construcrot.prototype),知识为了看的更清楚才分开写了
//执行构造函数,改变this指向,使得obj能访问到构造函数中的属性
let result = Constructor.apply(obj, arguments);
// 加上这一步的作用就是如果构造函数有返回值(一般我们不会这样去做),那么就返回,否则,默认返回obj
return result instanceof Object ? result : obj;
}
// 试验一下
function Animal(type) {
this.type = type;
}
Animal.prototype.say = function() {
console.log('say');
}
let o = mockNew(Animal, '哺乳类');
o.say();
console.log(o.type);
顺便提一句,既然我们用到了instanceof
,那我们写一下其原理是怎么实现的吧~其实就是根据__proto__
属性,在原型链上不断查找,instanceof
左边是实例对象,右边是构造函数或类:
function instanceOf (A, B) {
B = B.prototype
A = A.__proto__
while (true) {
if (A === null) {
return false
}
if (A === B) {
return true
}
A = A.__proto__
}
}
// 尝试一下
class A {
}
let a = new A()
let res = instanceOf(a, A)
console.log(res)
call实现原理
Function.prototype.call = function (context, ...args) {
// 如果context为真值,那么将其包装成一个对象
context = context ? Object(context) : window
// 创建一个独一无二的fn名
let fn = Symbol()
// 将this赋值给context.fn属性,这里的this就是指调用call的那个函数
context[fn] = this
// 这样,在调用这个函数的时候,因为被放置在了context里边,所以this就会指向context
context[fn](...args)
delete context[fn]
return result
}
apply实现原理
apply
和call
是类似的,只不过传参不一样,能看懂call
的话,那么apply
也不在话下!
Function.prototype.apply = function(context, arr) {
context = context ? Object(context) : window;
let fn = Symbol();
context[fn] = this;
let result = arr ? context[fn](...arr) : context[fn]();
delete context[fn];
return result;
};
bind实现原理
bind
在内部使用了apply
,返回一个新的函数,所以代码如下:
Function.prototype.bind = function (context, ...args) {
return (...argument) => {
return this.apply(context, [...args, ...argument])
}
}
// 尝试一下
let obj = {
name: 'jw'
}
function fn (a, b) {
console.log(this.name, a + b)
}
let bindFn = fn.bind(obj, 9)
bindFn(3)
结尾
关于this
的问题,我们看完这篇文章,大致就能比较清楚的判断了,如果在项目中,因为this
指向出现一些问题,也能及时的排查出来。下次看到了绕来绕去代码,this
乱指,如果是在面试中,不妨耐着性子一点点梳理,如果是你同事写出来的代码,一巴掌直接呼过去了就。