探究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)
报错的原因是什么呢,我们一起来分析一下:
- 我们定义了一个构造函数A与一个未申明值的变量a,使用apply方法让a调用构造函数A
- 在严格模式下apply不会将undefined 转换为全局变量,非严格模式下则会进行转换
- 所以执行 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()
关于我
与大多数人一样,呆在一个十八线小城市,每天朝九晚五,边上泡杯枸杞茶,拿着工具切一天图。但是我不想一直站在巨人的肩上,我想去成为那个巨人。这是我第一次到掘金发文章,希望能够得到大家的支持,往后也会持续更新,更多的会去探讨一些底层的实现原理与机制,谢谢大家。
参考文献:
js基础-面试官想知道你有多理解call,apply,bind?[不看后悔系列]