前言
this关键字,想必大家都有所耳闻,在一些开发语言当中,比如 Java ,我们经常在构造函数中使用this来指向自身从而进行一些赋值的操作。在 JavaScript 的开发当中,this 有复杂的运行机制,我们在使用前需要来了解一些常规的知识,避免错误使用❌。
误解 --> 指向自身
对于this我们最大的误解就是 他指向函数自身 ,我们要明确这是在某些时候有效,更不能当作结论来记忆,通过下面的代码我们就可以清晰地认识到这个误区。
var num = 0
function foo(){
var num = 1;
console.log(this.num); // ?
}
foo();
在foo函数中我们定义了一个变量num,并且打印 this.num 的值,但是在调用函数之后,打印出来的却是 0,如果真如我们所想,this指向函数自身,那 this.num 就应该打印出 1,但是却是0,仿佛this指向的是全局一样。无论结果如何,this指向自身的说法都已不攻自破,下面我们来一起学习一下this的指向问题。
绑定规则
既然要学习this,首先我们要知道this是什么。第一条需要牢记的-->函数在执行的时候才会产生自身的this指向。我们已经知道函数被调用时,会创建一个执行期上下文,用于记录函数在哪里被调用,函数的调用方式,传入的参数等信息,this也被记录其中,会在函数执行的过程中用到。因此 this是在函数被调用时发生的绑定,它指向什么完全取决于函数在哪里被调用。
1.默认绑定
var num = 0;
function foo(){
var num = 1;
console.log(this.num); // ?
}
foo();
foo函数在全局被执行,因此这里的this指向全局,打印出来的自然是全局的变量num。
默认绑定,你可以把他看作是其他绑定无法应用或失效时的默认形式。this一般会指向全局,为什么是一般呢?在 严格模式(strict mode) 下,全局对象无法进行绑定,导致this只能绑定在undefined身上。
还有,如果你的代码运行在浏览器上,全局对象就是window;在node上运行,全局对象就是global。这也会导致打印出来的结果不相同。全局定义的 变量num 会自动挂载在 window对象 上,所以在浏览器上this指向window,就可以找到 变量nu m;在node环镜下,this指向global,而 global对象 里并没有变量num,就会打印 undefined。
当然即使是在浏览器环镜下,当你的全局变量是这样定义的 let num = 0; ,打印的也会是undefined,因为 let 申明的变量不会挂载到 window对象 上,你自然也就明白了。
2.隐式绑定
function foo(){
console.log(this.a); // ?
}
var obj = {
a: 5,
foo: foo
}
obj.foo();
在这里foo函数被obj对象引用式地调用了,打印出来的 this.a 为5,就是obj的a属性值,很神奇,这里的this指向了obj对象。
当函数引用有上下文对象时,隐式绑定的规则就会把函数调用中的this绑定到这个上下文对象。 听着有些绕,翻译过来就是 谁调用就指向谁。
当然这种绑定并不牢靠,代码稍不留神就会出现 隐式丢失 的现象。就像下面一样:
var a = '123';
function foo(){
console.log(this.a); // ?
}
var obj = {
a: 2,
foo: foo
}
var bar = obj.foo;
bar();
在这里 obj对象 中也有 foo函数 作为引用属性,但是在全局,变量bar 引用到了 foo函数 ,并且执行了bar函数 (补充:像JavaScript这样的弱类型语言,在申明变量的时候并不会指定变量类型,只有在赋值的时候,根据所赋予的值的类型来确定变量类型。这里 变量bar 被赋值了 foo函数 本身,所以 变量bar 就是函数类型。又因为函数的赋值是一种引用,及传递的是该函数的引用地址,所以这里的 bar函数 和 foo函数 其实就是一个函数。) 结果就相当于将 foo函数 放置在全局执行,发生隐式丢失,启用默认绑定。
像这种直接赋值的丢失我们可以一眼看出,但是像下面这样的就会有些绕:
var a = '123';
function foo(){
console.log(this.a); // ?
}
function bar(fn){
fn();
}
var obj = {
a: 2,
foo: foo
}
bar(obj.foo);
这里直接就定义了一个 bar函数 ,它在调用的时候将 obj.foo(就是foo函数本身) 作为实参传递了进去,并且在其内部调用,就相当于 foo函数 在 bar函数 里面被执行了。那么是不是就说明 foo函数 的this就指向 bar函数 呢?其实不是的,我们说的是 谁调用指向谁 ,foo函数的调用只是在bar函数里,bar函数并没有主动调用foo函数,所以这里依然启用默认绑定,我们称这里为参数赋值的丢失。
3.显示绑定
除了隐式绑定这样不太牢靠的绑定,我们自然有牢靠些的显示绑定。
function foo(){
console.log(this.a);
}
var obj = {
a: 2,
}
// 1.this指向
foo.call(obj); //2
foo.apply(obj); //2
var bar = foo.bind(obj);
bar(); //2
我们分别调用了 call、apply、bind ,并且也没有在对象内部包含一个指向函数的属性,就实现了foo函数的this指向obj对象。没错,call、apply、bind可以在调用foo函数时强制把它的this绑定到obj上。
那都可以进行强制绑定,call、apply、bind 三者自然也有不一样的地方。
function foo(a,b,c){
console.log(a,b,c);
}
var obj = {
a: 2,
}
// 2.传参问题
foo.call(obj,1,2,3); //1 2 3
foo.apply(obj,[1,2,3]); //1 2 3
var bar = foo.bind(obj);
bar(1,2,3); //1 2 3
- call : 参数依次传递。
- apply :参数以数组的形式进行传递。
- bind :会返回一个新函数,在新函数被调用时依次传入参数。
可以看到,我们这里传入的第一个参数 (来用作this的绑定对象) 都是 Object类型 ,如果是简单类型会如何呢?
function foo(a,b,c){
console.log(this);
}
var obj = {
a: 2,
}
// 3.参数不为对象的时候
foo.call(1); //[Number: 1]
foo.call('a') //[String: 'a']
foo.call(true) //[Boolean: true]
foo.call(undefined) //window
foo.call(null) //window
参数不为对象的时候, 简单类型有包装类就变为包装类 (补充:原始值会被转换成它的对象形式,也就是 new String(...)、new Boolean(...)、new Number(...),这通常被称为“装箱”) ,像undefined和null没有包装类就会绑定失败,默认绑定在window上。
显示绑定我们会用到 call、apply、bind 这三个方法,当我们在调用 JavaScript 的一些内置函数,会提供一个可选的参数,来确保你的回调函数使用指定的this,其实现就是通过 call、apply、bind 三者。
let obj = {
a: 2,
};
let arr = [1,2,3,0];
// 4.API 的调用
arr.forEach(function(item,index,arr){
console.log(this); //obj
},obj) // <-- 注意这里的第二个参数
4.new绑定
function Person(){
this.a = 1;
}
var person = new Person();
console.log(person); //Person {a: 1}
new绑定,顾名思义我们会用一个 构造函数来new一个实例化对象,并且 this就指向实例化对象自身。
那构造函数在创建实例化对象的时候发生了什么事呢?
- 在函数体最前面隐式的创建一个this={}
- 执行函数中的代码(往this身上挂载属性)
- 将对象的隐式原型更改为构造函数的显示原型
- 隐式的返回这个this(构造函数中只有显式的return引用类型,才能干扰隐式的return值)
我们先忽略第三条提到的 ‘原型 ’ 概念,之后的文章会有介绍,拆开来说,new绑定就是这么简单。
那第四条提到的 ‘显式的return引用类型 ’ 是什么意思呢?我们这样来看:
function Person(){
this.a = 1;
// return的值为引用值,会改变this指向
return [1,2];
}
var person = new Person();
console.log(person); //[1,2]
构造函数在创建实例化对象的时候,最后一步会把封装好的this隐式的 (指代码上没有体现出有return,但是运行过程中却有return出了结果,属于V8引擎自动完成的) 传递出去,我们显示的在构造函数的内部添加上return,会覆盖V8的隐式return,从而修改了实例化对象。当然必须是引用类型,是简单类型(像number、string、boolean、null、undefined)就不会有干扰。
this的绑定规则 就如上面所呈现的,光有规则还不够,我们要明白每条规则之间的优先级顺序,之后会有文章推出来介绍 this的优先级问题 ,敬请期待。