this指向让多少人白了发秃了头?

215 阅读8分钟

探究js中的this

开始之前我们先来看一道题,带着你的答案,看到最后,看看是否如你所想的那样,答案会放在评论区。

window.name = 'window'
var name = 'window2'
function A() {
    this.name = 'a'
    b()
    function b() {
        var name = 'b'
        console.log(this.name)		// ?
    }
}
let a 
A.apply(a) 

本篇文章将会涉及的知识点:

如何判断this指向

new的实现原理

apply、call方法的实现原理

bind方法的实现原理

开始前对涉及到的名词做一些解释:

执行上下文:即当前javaScript代码解析与运行时的所在环境。

全局执行上下文:这是默认的,直接写的代码都在全局执行上下文中。

函数执行上下文:每当创建一个函数时,就会创建一个函数执行上下文。

ok,下面我们正式开始!

1.严格模式与非严格模式

this在严格模式与非严格模式的区别在于:

严格模式下未明确指定this指向时,那么this会保持为undefined;

非严格模式下未明确指定this指向时,那么this会被赋值为全局变量(window or global);

我们来看一下之前的题目在严格模式下是如何表现的:

"use strict";
var name = 'window2'
function A() {
    this.name = 'a'			// Cannot read property 'name' of undefined
    b()
    function b() {
        var name = 'b'
        console.log(this.name)
    }
}
let a				
A.apply(a)			

报错的原因是什么呢,我们一起来分析一下:

  1. 我们定义了一个构造函数A与一个未申明值的变量a,使用apply方法让a调用构造函数A
  2. 在严格模式下apply不会将undefined 转换为全局变量,非严格模式下则会进行转换
  3. 所以执行 this.name 等价于 undefined.ame 自然报错了

知道了原因,那我们来把a定义成一个对象,再来试试:

"use strict";
var name = 'window2'
function A() {
    this.name = 'a'			
    b()
    function b() {
        var name = 'b'
        console.log(this.name)	// Cannot read property 'name' of undefined
    }
}
let a = {}				
A.apply(a)	

那这为啥又报错了?

我们可以看到b()函数虽然是在A构造函数中使用的,但是并未显示声明由谁来调用的。

所以就如前面所说,严格模式下未明确指定this指向时,那么this会保持为undefined,自然也就报错了。

如果想正常运行,需明确指定this的指向:

b.apply(this)

聊到这,你再回头看看开篇的问题,应该就知道答案是啥了,下面我们一起来看看箭头函数。

2.普通函数与箭头函数

在这里简要介绍一下箭头函数与this相关的知识点:没有函数执行上下文,就没有this指向,也就没有构造函数。

我们拿上边的例子改造下,先来看看普通函数的表现:

var name = 'window2'
function A() {
    this.name = 'a'
    let b = function() {
        var name = 'b'
        console.log(this.name)		// 'window2'
    }
    b()
}
let a = {}
A.apply(a)

可以看到普通函数就和我们上一节得出的结论一样,没有显示指定this,b函数默认被全局变量调用

下面我们把普通函数改为箭头函数,看看它的表现:

var name = 'window2'
function A() {
    this.name = 'a'
    let b = ()=> {
        var name = 'b'
        console.log(this.name)		// 'a'
    }
    b()
}
let a = {}
A.apply(a)

可以看到,在箭头函数中,直接引用了a对象上的name属性,也就是说:箭头函数中的this将会引用外层的this指向。

所以和上一节提到的可以使用apply方法显式绑定this外,我们在构造函数中还能够使用箭头函数的特性,来隐式指定this的指向。

3.new发生了什么

this和new有啥关系呢?

来看之前我们的例子,我们使用apply方法将构造函数指向了a变量,那如果我们直接用new实例化a,那结果是否完全一致呢?要说清楚new发生了什么,首先你要知道javaScript是一种原型模式的语言,我们使用构造函数创建实例,其实创建的过程,就是将构造函数的一切复制给所谓的实例。new的模拟实现:

var objectFactory = function () {
    var obj = new Object(),    // 从 Object.prototype 上克隆一个空的对象         
        Constructor = [].shift.call(arguments);    // 取得外部传入的构造器

    obj.__proto__ = Constructor.prototype;    // 指向正确的原型 or Object.setPrototypeOf  
    var ret = Constructor.apply(obj, arguments);    // 借用外部传入的构造器给 obj 设置属性 

    return typeof ret === 'object' ? ret : obj;     // 确保构造器总是会返回一个对象 
};
var a = objectFactory(A);

我们可以看到new的过程:

=> 创建一个空对象

=> 将构造函数原型指向空对象的隐式原型

=> 空对象调用构造方法,把它内部的属性方法统统拿给自己

=> 返回对象

与我们今天探讨的this有关的就是第三步,使用apply方法调用构造函数,与前面的例子中别无二致。

看到这里你可能就要问了,前面的例子中用apply方法改变this指向,new中也使用apply方法改变this指向,那apply它凭啥就能改变this指向呢?下面我们就一起来看看。

4.apply发生了什么

其实apply的实现,大家自己肯定都写过只是并没有意识到,我们来看看这个对象:

let obj = {
    name: 'even',
    getName: function () {
    	console.log(this.name)		// 'even'
    }
}
obj.getName()

这里为啥是even? 因为getName方法创建了函数执行上下文,该函数被obj对象调用,该方法的this指向就变成了这个obj对象。所以输出了它的name :even。

那现在我有另一个对象:

let obj2 = {
    name: 'even2'
}

如果不使用apply、bind、call方法,我需要他调用getName方法应该怎么做?

其实很简单,我们没有的,直接抢过来不就完事儿了

let obj2 = {
    name: 'even2'
}
obj2.getName = obj.getName
obj2.getName()						// 'even2'

那最后我们在把obj2的getName 干掉,那是不是就实现了apply方法?

apply的基础实现:

=> 将需要执行的方法复制到自己名下

=> 运行该方法

=> 扔掉这个方法,吹个口哨,假装无事发生

好了,apply的实现就说到这里,call方法也是如此。这是最基础的实现,想要看具体实现:木易杨前辈

5.bind发生了什么

这是es6新增的方法,帮我们解决了在复制函数时,由于执行上下文的不同导致this指向不同的问题。我们先来看下面这个例子:

var name = 'window'
let obj = {
    name: 'even',
    getName: function () {
    	console.log(this.name)
    }
}
let getName = obj.getName
getName()		// 'window'

为啥不是even? 因为你复制函数,却没法复制执行上下文。

当前的执行上下文是全局的,自然获取到的this.name 是全局变量上的name。

解决的方式就是使用bind方法,直接绑定this

let getName = obj.getName.bind(obj)
getName()		// 'even'

我们从上面的例子中可以看出bind方法的一些特性:

=> 返回一个捆绑了执行上下文的函数,需要手动执行

我们从上面例子看不出的一些特性:

=> 绑定执行上下文后,就无法被改变(即第一绑定了this,后续你再怎么改,也改不掉)

=> bind方法第二个函数后可传参,调用时可二次传参,多次进行bind操作可添加参数

看完这些,你品品,你仔细品品,这是个啥。

这不都是柯里化函数的特性嘛。下面我们一起来用柯里化函数实现bind方法:

Function.prototype.bind2 = function () {
    var slice = Array.prototype.slice						// 需多次调用 提取
    var thatFunc = this										// 保存 当前调用函数
    var thatArg = arguments[0]								// 保存 执行上下文
    var args = slice.call(arguments, 1);					// 保存 剩余参数
    return function () {
        var funcArgs = args.concat(slice.call(arguments))	// 合并参数
        return thatFunc.apply(thatArg, funcArgs);			// 传入上下文使用apply方法调用函数
    }
}

心理活动:这就完了?

现实:嗯,这就完了。

那刚才提到特性第二条,为啥绑定了上下文之后就无法被改变呢,特性第三条又是如何实现的呢?

let getName = obj.getName.bind2(obj)		// getName方法 与obj.getName等价嘛?
getName = getName.bind2(obj)				// 将返回后的函数再次传入 return的又是个方法

你多次对同一个方法进行bind操作,其实就是在对他进行套娃操作,之所以它的执行上下文没有被改变,是因为最初保存它的那个变量始终都没有被改变,而这些递归操作最后执行的函数还是我们第一次调用的getName函数,并且依旧是那个执行上下文。

那为什么参数累加了呢?

因为每次递归的时候都把funcArgs回传了呀,傻孩子。

总结

关于this,归根结底你需要注意的是,当前的执行上下文是什么,是谁在调用。当程序足够复杂的时候,多层嵌套的时候,推荐使用显示调用的方式,利用apply、bind等方法进行调用,可以避免一系列问题并减少维护成本。

最后再附赠一题给大家,单纯为了搞你而搞你 hhhh! 答案就不贴了,想不明白可以自己动手跑一下。

let name = 'window'
let name2 = 'window2'
let obj2 = {
	name: 'par',	
    name2: 'par2',
    b: function(){ return this.name2},
    son: {
        name: 'even',
        name2: this.name,
        b: function(){ return this.name2 },
        getName: function() {
            console.log(this.name)       
            console.log(this.name2)     
            console.log(this.b())      
        },
        getName2: ()=>{
        	obj2.son.getName.apply(this)
        }
    }
}
obj2.son.getName()
obj2.son.getName2()

关于我

与大多数人一样,呆在一个十八线小城市,每天朝九晚五,边上泡杯枸杞茶,拿着工具切一天图。但是我不想一直站在巨人的肩上,我想去成为那个巨人。这是我第一次到掘金发文章,希望能够得到大家的支持,往后也会持续更新,更多的会去探讨一些底层的实现原理与机制,谢谢大家。

参考文献:

MDN——this

MDN——apply方法

js基础-面试官想知道你有多理解call,apply,bind?[不看后悔系列]

你不知道的js中关于this绑定机制的解析[看完还不懂算我输]

木易杨前端进阶