this 深度解析
最近,重温曾探的《JavaScript设计模式与开发实践》【下载相关学习资料 】 在各种案例中,关于this、call、apply 的使用及奇平凡,有些设计模式的案列相对复杂,各种来回复杂的调用,让人有点丈二的和尚摸不着头脑,于是又重温一下this,这里我会由浅入深的对自己再次的学习进行一些总结。
this 指向
引用《javascript高级程序设计第三版》【下载相关学习资料 】中的官方解析。this 总是指向一个对象,而具体指向哪个对象是在运行时基于函数的执行环境动态绑定的,而非函数被声明时的环境。(简单来说就是:谁调用就指向谁)
this 的指向大致可以分为以下 4 种:
- 作为对象的方法调用。
- 作为普通函数调用。
- 构造器调用。
- Function.prototype.call 或 Function.prototype.apply 或 Function.prototype.bind 调用
- 箭头函数调用
- DOM对象的处理函数
- 原型链中的this
先上基础案列
作为对象的方法调用
当函数作为对象的方法被调用时, this 指向该对象
var obj = {
a: 1,
getA: function(){
alert ( this === obj ); // 输出: true
alert ( this.a ); // 输出: 1
}
};
obj.getA();
这里的getA()被obj这个对象调用,所以getA()内部中的this指向obj
作为普通函数调用
最为普通函数方式,此时的 this 总是指向全局对象
方式1:下面我称之为fun1
window.name = 'window';
var getName = function(){
return this.name;
};
console.log( getName() ); // 输出: globalName
方式2:下面我称之为fun2
window.name = 'window';
var myObject = {
name: 'sven',
getName: function(){
return this.name;
}
};
var getName = myObject.getName;
console.log( getName() ); // globalName
观察上面两种方式,fun1 和 fun2 的区别,fun2和 作为对象的方法调用 的唯一区别是将 myObject.getName 先赋值给变量 getName,然后再调用,这种写法其实就是下面的代码形式:
window.name = 'window';
var getName = function(){
return this.name;
};
console.log( getName() ); // globalName
和 fun1 一样,所以就一目了然了
作为构造器调用
当用 new 运算符调用函数时,该函数总会返回一个对象,通常情况下,如果构造器不显式地返回任何数据,或者是返回一个非对象类型的数据,构造器里的 this 就指向返回的这个对象,
var MyClass = function(){
this.name = 'sven';
return 'anne'; // 返回 string 类型
};
var obj = new MyClass();
console.log(obj) // {name: 'sven'}
alert ( obj.name ); // 输出: sven
这里顺便讲一下为啥是这样,或者说 new MyClass 到底做了什么?
var MyClass = function(){
this.name = 'sven';
};
// new 的四步分别做了什么
var newFun = function() {
var obj = new Object(); // 创建一个对象
var fn = Array.prototype.shift.call(arguments);
obj.__proto__ = fn.prototype; // 将新对象的原型指向构造函数的原型
fn.call(obj); // 改变 this 的作用域
return obj // 返回新对象
}
newFun(MyClass)
但用 new 调用构造器时,还要注意一个问题,如果构造器显式地返回了一个显示的 object 类型的对象,那么此次运算结果最终会返回这个对象,而不是我们之前期待的 this:
var MyClass = function(){
this.name = 'sven';
return { // 显式地返回一个对象
name: 'anne'
}
};
var obj = new MyClass();
cosole.log( obj); // 输出: {name: 'anne'}
- 这里很明显不是我们想要的 {name: ‘sven’}, 这又是为啥,又是怎么实现的?
- 在刚才的代码基础上,我们再稍作改动
var MyClass = function(){
this.name = 'sven';
return { // 显式地返回一个对象
name: 'anne'
}
};
// 内部返回对象是如何进行判断的
var newFun = function() {
var obj = new Object(); // 创建一个对象
var fn = Array.prototype.shift.call(arguments);
obj.__proto__ = fn.prototype; // 将新对象的原型指向构造函数的原型
/*********以下为修改代码**********/
var innertObj = fn.call(obj); // 改变 this 的作用域
// 判断函数内部是否返回了一个对象
if (innertObj && (innertObj instanceof Object)) {
return innertObj
}
/*********以上为修改代码**********/
return obj // 返回新对象
}
newFun(MyClass)
call、apply、bind
call、apply 方法
用 call 和 apply 可以动态地改变传入函数的 this:
var obj1 = {
a: 1,
b: 2,
caculate: function(c,d){
return this.a + this.b + c + d;
}
};
var obj2 = {
a: 10,
b: 20,
};
console.log( obj1.caculate(3,4) ); // 输出: 10
console.log( obj1.caculate.call( obj2, 3, 4 ) ); // 输出: 37
console.log( obj1.caculate.apply( obj2, [30, 40] ) ); // 输出: 100
call 和 apply 的用法唯一的不同就是传参的方式不同,参见上面两种调用方式
bind 方法
用 bind 也可以动态地改变传入函数的 this,但是Ta是永久绑定,一旦绑定不会在改变:
?:还是用上面的例子
// 调用方法1
var caculate = obj1.caculate
var fun1 = caculate.bind({a: 44, b: 5})(3,4)
var fun2 = caculate.bind({a: 22, b: 55})(3,4)
console.log(fun1()) // 56
console.log(fun2()) // 84
// 调用方法2
var fun3 = caculate.bind({a: 44, b: 5})(3,4) // fun3 被绑定
var fun4 = fun3.bind({a: 22, b: 55})(3,4) // fun4 改变 fun3 的作用域
console.log(fun1()) // 56
console.log(fun2()) // 56
思考???
- 方法 2 中的 fun4 再次改变 fun3 的作用域并没有生效?
- 作为一个合格程序员,我会有疑问,那问啥上面 fun1 和 fun2 中的 caculate 没有永久绑定到 {a: 44, b: 5} 中呢???
原因其实很简单,那是因为 caculate 是没有使用bind来改变其作用域的,谁用bind绑定后,谁的作用域就永久生效,不会再被改变
为了进了一步给小伙伴们证实一下,请看下面的?:
var obj1 = {
a: 1,
b: 2,
caculate: (function(c,d){
return this.a + this.b + c + d;
}){a: 100, b: 200} // 手动强行绑定
};
var caculate = obj1.caculate
var fun1 = caculate.bind({a: 44, b: 5})
var fun2 = caculate.bind({a: 22, b: 55})
console.log(fun1(3,4)) // 307
console.log(fun2(3,4)) // 307
箭头函数
官方是这么解释的: 在箭头函数中,this与封闭词法环境的this保持一致。在全局代码中,它将被设置为全局对象
表示每台看懂,肿么办,撸起袖子就是干啊,看?:
var a = 'window'
var obj1 = {
a: 1,
b: 2,
caculate1:() => {
console.log(this.a) // this 从 window 中引用
},
caculate2: function() {
return () => console.log(this.a) // caculate2 和 obj1 构成了封闭的词法环境,此时的 链式调用,this 指向 obj1
},
bar: function() {
console.log(this.a) // 同上
}
};
obj1.caculate1() // window
obj1.caculate2()() // 1
obj1.bar() // 1
要弄明白为什么是这样的打印结果,我们得追本溯源,箭头函数自身是没有 this 关键字的,而是从外部通过链式引用而来的 只要明白这一点就不难解释上面的输出结果了
DOM事件的处理函数
当函数被用作事件处理函数时,它的this指向目标元素
// 被调用时,将关联的元素变成蓝色
function bluify(e){
console.log(this === e.currentTarget); // 总是 true
// 当 currentTarget 和 target 是同一个对象时为 true
console.log(this === e.target);
this.style.backgroundColor = '#A5D9F3';
}
// 获取文档中的所有元素的列表
var elements = document.getElementsByTagName('*');
// 将bluify作为元素的点击监听函数,当元素被点击时,就会变成蓝色
for(var i=0 ; i<elements.length ; i++){
elements[i].addEventListener('click', bluify, false);
}
当代码被内联on-event 处理函数调用时,它的this指向监听器所在的DOM元素:
<button onclick="alert(this.tagName.toLowerCase());">
Show this
</button>
上面的 alert 会显示button。注意:只有外层代码中的this是这样设置的:
<button onclick="alert((function(){return this})());">
Show inner this
</button>
原型链中this
var o = {
f: function() {
return this.a + this.b;
}
};
var p = Object.create(o);
p.a = 1;
p.b = 4;
console.log(p.f()); // 5
不做过多解释了,其实本质和对象调用是一样的,只不过 f() 是继承而来,仅此而已,我们只需要记住,谁调用就指向谁是没毛病的
案例:
var a=3;
var myObject = {
foo: "bar",
func: function() {
var self = this;
console.log("outer func: this.foo = " + this.foo); // bar
console.log("outer func: self.foo = " + self.foo); // bar
(function() {
console.log("inner func: this.foo = " + this.a); // 3 这里的this是当作普通函数调用的,所以指向全局
console.log("inner func: self.foo = " + self.foo); // bar self 其实就是 myObject, 你可以理解被绑定了
console.log.bind(self, "inner func: self.foo = " + self.foo);
}());
}
};
myObject.func();
思考一下?
var a=3;
var myObject = {
foo: "bar",
func: function() {
var self = this;
console.log("outer func: this.foo = " + this.foo); // bar
console.log("outer func: self.foo = " + self.foo); // bar
(function() {
console.log("inner func: this.foo = " + this.a); // 3 这里的this是当作普通函数调用的,所以指向全局
console.log("inner func: self.foo = " + self.foo); // bar self 其实就是 myObject, 你可以理解被绑定了
console.log.bind(self, "inner func: self.foo = " + self.foo);
}());
}
};
var fun = myObject.func;
fun() // 又会输出什么
小结
关于this的使用场景以及相关的原理总结到这里,后续会持续跟进一些前端必须掌握的知识
其它前端性能优化:
- 图片优化——质量与性能的博弈
- 浏览器缓存机制介绍与缓存策略剖析
- webpack 性能调优与 Gzip 原理
- 本地存储——从 Cookie 到 Web Storage、IndexDB
- CDN 的缓存与回源机制解析
- 服务端渲染的探索与实践
- 解锁浏览器背后的运行机制
- DOM 优化原理与基本实践
- Event Loop 与异步更新策略
- 回流(Reflow)与重绘(Repaint)
- Lazy-Load
- 事件的节流(throttle)与防抖(debounce
- 前端学习资料下载
- 技术体系分类
前端技术架构体系(没有链接的后续跟进):
- 调用堆栈
- 作用域闭包
- this全面解析
- 深浅拷贝的原理
- 原型prototype
- 事件机制、
- Event Loop
- Promise机制、
- async / await原理、
- 防抖/节流原理
- 模块化详解、
- es6重难点、
- 浏览器熏染原理、
- webpack配置(原理)
- 前端监控、
- 跨域和安全、
- 性能优化(参见上面性能优化相关)
- VirtualDom原理、
- Diff算法、
- 数据的双向绑定
- [TCP协议(三次握手、四次挥手)](https://blog.youkuaiyun.com/woleigequshawanyier/article/details/85223642
- DNS域名解析
其它相关
欢迎各位看官的批评和指正,共同学习和成长
希望该文章对您有帮助,你的 支持和鼓励会是我持续的动力