你不知道的 JS 系列之全面解析 this
任何足够先进的技术都和魔法无异。this 关键字是 JavaScript 中最复杂的机制之一, 搞懂它很重要。
一、this 是什么?
this 是 JavaScript 中的关键字,在常见的面向对象语言中都有 this 的身影,相较下 JavaScript 中的 this 比较特殊,特殊在它会在执行期间动态改变指向。this 一般定义在函数中,如果按英文解释,很容易产生误解,this 既不指向函数自身,也不指向函数的词法作用域。它在运行时进行绑定,它指向什么完全取决于函数在哪里被调用。
二、为什么要用 this ?
this 提供了一种更优雅的方式来隐式 “传递” 一个对象引用,让我们可以避免显示传递上下文对象引起的代码混乱,因此可以将 API 设计 得更加简洁并且易于复用。
三、4 条绑定规则
我们普遍知道的规则是,谁调用 this , this 指向谁。但这只是下面要讲的规则中的一条。
1、默认绑定
我们在写原生 JS 时,会直接定义一个函数,然后直接进行独立的函数调用,此时应用默认绑定规则。
举例说明:
function run(){
console.log(this.a);
}
var a = 1;
run();
复制代码
浏览器环境下,在非严格模式中,var 在全局作用域中定义的变量会自动添加到 window 对象下,在全局环境下执行函数 run , 就理解为 run 在 window 中被调用,此时的 run 函数是定义在 window 对象中的。上面讲 this 指向什么完全取决于函数在哪里被调用,所以此时 this 指向 window, window.a 自然输出 1。
严格模式中,var 声明的变量不会自动绑定到 window 对象下,同理 run 函数也不是定义在 window 对象中的, run() 独立执行,执行 RHS 右查询 this ,查无此值,给到 undefined , undefined.a 自然报错,且报错类型为 TypeError , 类型错误,因为 undefined 不是一个对象。
默认绑定下,this 是 window 、undefined 二者其一,取决于是否是严格模式。
2、隐式绑定
这就是我们常说的,谁调用 this , this 指向谁,举例说明:
function run(){
console.log(this.a);
}
var obj = {
a: 1,
run
}
obj.run();
复制代码
注意: 隐式丢失的发生,就是我们常说的 this 丢失,多发生在函数赋值,举例说明:
function run() {
console.log( this.a );
}
var obj = {
a: 2,
run
};
var fn = obj.run;
var a = "welcome";
fn();
复制代码
你产生的困惑发生于var fn = obj.run;
赋值语句,很好理解,不要把问题复杂化。正如我们所知的,JS 中函数即对象,obj.run
代表对run
函数对象的引用,但此时执行的是赋值语句,赋值语句右侧一般执行 RHS 右查询,即查询具体的值,所以此时fn
直接指向run
函数本身,fn()
的执行等同于直接执行run()
, 所以你的疑惑自然消除,再看一个例子:
function run() {
console.log( this.a );
}
var obj = {
a: 2,
run
};
var a = "welcome";
setTimeout( obj.run, 100 );
复制代码
setTimeout
函数第一个参数为函数,我们给它起个名字 fn
, 参数obj.run
的传入发生隐式赋值 fn = obj.run
, 讲到这里,再结合上面,你该明白了。
3、显示绑定
call、apply、bind 实现的绑定我们称为实现显示绑定,这里不做过多解释,解释一个我们常常忽略的可选参数,看下面一段代码:
function run(param) {
console.log( param, this.id );
}
var obj = {
id: "welcome"
};
[1, 2, 3].forEach( run, obj );
复制代码
这里数组的forEach()
方法,第二可选参数传递给函数的值一般用做 "this" 值。 如果这个参数为空, "undefined" 会传递给 "this" 值。
4、new 绑定
我们都知道 new 一个函数,函数内部的 this 指向新生成的实例,但这是为什么呢?我们需要知道 new 的过程中发生了什么,当然我们这里不会讲开辟什么堆栈空间之类的,这不在我们的讨论范围。
new 就是一个可以调用普通函数的操作符,使用 new 来调用函数,或者说发生构造函数调用时,会自动执行下面的操作:
创建(或者说构造)一个全新的对象,这个新对象会被执行 [[ 原型 ]] 连接,这个新对象会绑定到函数调用的 this,如果函数没有返回其他对象,那么 new 表达式中的函数调用会自动返回这个新对象。
四、优先级
new 绑定 > 显示绑定 > 隐式绑定 > 默认绑定
所以我们见到 this , 该根据此优先级规则来判别。
五、被忽略的 this
一些情况下,我们会在显示绑定的时候传入 null, 例如Math.max.apply(null,[1,2,3])
, 我们并不关心传入的 this , 只是传入一个占位置,此时应用默认绑定。 举个例子:
function run() {
console.log( this.a );
}
var a = 1;
run.call(null);
复制代码
这里建议传入一个更安全的对象 Object.create(null)
,Object.create(null)
创建的对象与{}
的区别在于,{}
上面还会有Object
原型上的toString()
等方法,而Object.create(null)
什么都没有。我喜欢用穷徒四壁来形容 {}
,那么 Object.create(null)
对比之下就是连四壁也没有。
六、箭头函数下的 this
我们都知道箭头函数没有自己的 this,它的 this 来自于外层作用域,也因为箭头函数没有自己的 this 这一特性,它不能被用作构造函数。 箭头函数不使用上面 this 的四种标准规则,函数执行时,它会捕获外层作用域的 this ,一经绑定再也无法修改,并且高于 new 绑定。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
上一篇: Vue 3 入门教程
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论