说说this
我们都知道Javascript中的「this」真的是个头痛的东西,今儿我们就来好好总结下这个「this」。
我记得之前这块内容,我直接先背了个「口诀」。
function fn() {
console.log(this);
}
// 1. fn(); // 指向window
// 2. fn(); // undefined(严格模式下)
// 3. a.b.c.fn() // 指向a.b.c
// 4. new fn() // 指向new的实例对象
// 5. () => { fn(); } // 箭头函数调用fn,this指向「外层代码库的this」
之前了解的不多,今天来详细解析一波。
首先我们来说说this的绑定规则。
(1)默认绑定
(2)隐式绑定
(3)显式绑定
(4)new绑定
以下全部在「浏览器环境」中。
(1)默认绑定
在不能应用「其他绑定规则」的时候,使用「默认绑定」,通常是作为「独立函数」进行调用。
function foo() {
console.log(this.name); // 'Jason'
}
var name = 'Jason';
foo();
在调用「foo()」的时候,应用了「默认绑定」,this指向了全局对象window(非严格模式下),严格模式下,指向「undefined」。
(2)隐式绑定
函数的调用是在「某个对象」上触发的,即调用位置上存在「执行上下文」。典型的形式如「xxx.fn()」。
var name = 'Jack';
function fn() {
console.log(this.name);
}
var obj = {
name: 'Jason',
foo: fn
};
obj.foo(); // 'Jason';
函数fn的声明在对象obj的外部,看起来是不属于obj对象的,但是在调用foo的时候,隐式绑定会把函数调用中的「this」(foo函数中的this)绑定到对应的「执行上下文」(此例中的obj)。注意:对象属性链只有最后一层会影响调用位置。
function fn() {
console.log(this.name);
}
let b = {
name: 'Jack',
foo: fn
}
let a = {
name: 'Jason',
friend: b
}
a.friend.foo();
上面代码中,foo函数的执行环境不是a,而是「a.friend」,即是「b」。
但是隐式绑定存在一个问题:绑定丢失。我们来看下边一段代码:
function fn() {
console.log(this.name);
}
var name = 'Jack';
let obj = {
name: 'Jason',
sayName: fn,
}
let say = obj.sayName;
say(); // 'Jack'
如果我们单看「obj.sayName」,执行上下文是对象obj,但是我们把「obj.sayName」赋值给了变量say后,调用say()后,函数fn的执行上下文就变为了全局变量(window)中。
针对这类问题,我们只要记住,形式为「xxx.fn()」才是隐式绑定,如果格式为「fn()」,前面什么都没有,那肯定不是隐式绑定,但是也不一定是「默认绑定」,下文中会解释。
除了上述的「绑定丢失」,还有一种绑定丢失的情况,就是发生在「回调函数」中,我们再看一个例子。
function fn() {
console.log(this.name);
}
var person1 = {
name: 'Jason',
sayName: function() {
setTimeout(function() {
console.log('Hello!', this.name);
})
}
};
var person2 = {
name: 'Jack',
sayName: fn
};
var name = 'Tom';
person1.sayName(); // 'Hello! Tom'
setTimeout(person2.sayName, 100); // 'Tom'
setTimeout(function() {
person2.sayName(); // 'Jack'
}, 200);
我们依次说说每次输出的原因。
- (1)setTimeout的回调函数中,this是「默认绑定」,在非严格模式下,指向全局变量「window」,所以输出「'Hello! Tom'」
- (2)第二条可能有些迷惑,不是说格式为「xxx.fn()」就是隐式绑定吗?然后执行上下文就是对象xxx?其实是这样的,对于setTimeout(fn, delay),第一个参数「fn」是「person2.sayName」,也就是说我们把「person2.sayName」赋值给了fn,然后执行了fn(),这样就跟person2无关系。
- (3)第三条虽然也是在setTimeout函数中,但是我们可以看到执行的是「 person2.sayName()」,所以是一个隐式绑定,因此函数的执行上下文是person2,跟当前的作用域无关系。
(3)显式绑定
显式绑定主要是通过call,apply和bind来显式的绑定this,call,apply和bind的第一个参数就是对应的this对象,call和apply的作用一样,只是call从第二个参数开始依次传入参数,而apply是直接把所有参数集成为一个数组放到第二个参数,bind会返回一个函数,在正式执行函数的时候,优先调bind第二个后的参数。来看看代码:
function fn() {
console.log(this.name);
}
var person = {
name: 'Jason',
sayName: fn
};
var name = 'Jack';
var sayName = person.sayName;
sayName.call(person); // 'Jason'
上述代码中,如果先不看最后一行,看倒数第二行「var sayName = person.sayName」,通过上述的讲述,可以认定到这一行,如果直接调用,所处的执行上下文是全局变量window,但是最后一行的call函数,指定了this的对象为person,则输出‘Jason’。
那么显式绑定是不是会出现绑定丢失呢?看下面的代码。
function fn() {
console.log(this.name);
}
var person = {
name: 'Jason',
sayName: fn
}
var name = 'Jack';
var Hi = function(fn) {
fn(); // 'Jack'
};
Hi.call(person, person.sayName);
乍一看最后一行的显式绑定,确实Hi函数的this绑定到了对象person上,Hi函数会接受一个参数fn,然后执行fn();此刻这个参数fn是call的第二个参数「person.sayName」,但是在执行fn的时候,相当于直接调用了sayName函数(person.sayName赋值给了参数fn,隐式绑定也丢了,),对应的是默认绑定。
那我们能不能继续还是想要绑定到person上?
function fn() {
console.log(this.name);
}
var person = {
name: 'Jason',
sayName: fn
}
var name = 'Jack';
var Hi = function(fn) {
fn.call(this); // 'Jason'
};
Hi.call(person, person.sayName);
其实只用在Hi函数中对fn使用call调用,因为刚刚我们说了最后一句话,HI的this对象绑定到了person对象上,那么我们在调用fn函数的时候再次显示绑定一次this,此时「fn.call(this)」中的this就是Hi函数的this对象(person)。
(4)new绑定
关于new会发生什么,可以具体看我之前的一篇文章(https://www.jianshu.com/p/6ea91eb41283
)。
简单来说:
(1)创建一个空对象,作为将要返回的对象实例。
(2)将这个空对象的原型,指向构造函数的「prototype」属性。
(3)将这个空对象赋值给构造函数内的「this」关键字。
(4)开始执行构造函数内的代码。
function Person(name) {
this.name = name;
}
var x = new Person('Jason');
console.log(x.name) // 'Jason'
(5)绑定例外
上述(1)~(4)已经基本参数了this绑定的规则,但是我们还是要补充点可能存在的问题。比如「绑定例外」。
如果我们在使用「显示绑定」的时候,第一个参数传了「null」或者「undefined」,这些值是会被忽略的,实际上应用的是「默认绑定」。
(6)绑定优先级
new绑定 > 显式绑定 > 隐式绑定 > 默认绑定
(7)箭头函数
箭头函数是ES6中的语法,带来了很多的便利,但是我们也有几个重要的注意点:
(1)函数体内的this对象,就是定义时所在的对象,而不是使用时所在的对象。
(2)不可以当作构造函数,也就是说,不可以使用new命令,否则会抛出一个错误。
(3)不可以使用arguments对象,该对象在函数体内不存在。如果要用,可以用 rest 参数代替。
(4)不可以使用yield命令,因此箭头函数不能用作 Generator 函数。
其中第一点就跟本文的主题this密切相关,我们来看两个例子。
var name = 'Jason';
var obj = {
name: 'Jack',
sayName: function() {
console.log(this.name); // 'Jack'
}
};
obj.sayName();
var name = 'Jason';
var obj = {
name: 'Jack',
sayName: () => {
console.log(this.name); // 'Jason'
}
};
obj.sayName();
上述两个例子非常相似,唯一不同点在于obj的sayName函数一个使用了普通函数形式(第一个例子),另一个使用了箭头函数(第二个例子),然后输出结果就不同了。
总的来说:普通函数的this是函数「运行时」绑定的,而箭头函数的this是函数「定义时」绑定的。
这里我们怎么理解「定义时」?我们可以说箭头函数的this和其外层代码库的this一样,或者说指向其父级执行上下文中的this。
上述第二个例子中,箭头函数本身跟sayName平级以key:value的形式,也就是说箭头函数本身存在于obj对象上,而obj对象所处的环境在全局变量window上,所以此例中的this.name其实是window.name。我们再看两个例子。
var name = 'Jason';
function Test() {
this.name = 'Jack';
let fn = function() {
console.log(this.name); // 'Jason'
};
fn();
}
var x = new Test();
var name = 'Jason';
function Test() {
this.name = 'Jack';
let fn = () => {
console.log(this.name); // 'Jack'
};
fn();
}
var x = new Test();
第一个例子中,一个普通函数赋值给了变量fn,然后直接调用fn(),是默认绑定,此时的this指向全局变量window,所以this.name就是window.name,输出‘Jason’。
第二个例子中,我们把箭头函数赋值为了变量fn,箭头函数中的this指向了父级执行上下文中的this,父级执行上下文中的this是通过new指向了其构造函数的实例(本例中的x),然后Test函数中第一句的「this.name = 'Jack'」,使得x.name为Jack。
我们再来一个例子练练手。
var obj = {
hi: function(){
console.log('1', this); // obj
return ()=>{
console.log('2',this); // obj
}
},
sayHi: function(){
return function() {
console.log('3', this); // window
return ()=>{
console.log('4', this); // window
}
}
},
say: ()=>{
console.log('5', this); // window
}
}
let hi = obj.hi();
hi();
let sayHi = obj.sayHi();
let fun1 = sayHi();
fun1();
obj.say();
我们来依次解释一下:
(1)第一条(1处)是由于「obj.hi()」调用函数,此时是隐式绑定,固然1处的this指向了obj。
(2)第2处,由于调用「hi()」,obj.hi()返回的是一个箭头函数,这个箭头函数中的this与外层代码库的this一样,外层代码库就是obj对象中的fn属性(这是一个函数),于是乎,跟1处的this是一样的,都指向obj。
(3)由于「obj.sayHi()」赋值给了变量sayHi,「sayHi」执行的时候,相当于原本是隐式绑定然后变为默认绑定,固然3处输出的是window。
(4)由于sayHi()赋值给了fun1,在「fun1()」执行的时候,执行的是一个箭头函数,然后我们就找这个箭头函数的外层代码库的this,就与3处的this相同,即输出window。
(5)由于「obj.say()」的执行,乍一看是一个隐式绑定,但是看到函数是箭头函数,obj的外层代码库所在的环境就是全局变量window,输出window。
那么箭头函数一定是静态的吗?
var obj = {
hi: function(){
console.log('1', this);
return ()=>{
console.log('2', this);
}
},
sayHi: function(){
return function() {
console.log('3', this);
return ()=>{
console.log('4', this);
}
}
},
say: ()=>{
console.log('5', this);
}
}
let sayHi = obj.sayHi();
let fun1 = sayHi(); // window
fun1(); // window
let fun2 = sayHi.bind(obj)(); // obj
fun2(); // obj
还是用上面的例子,我们看看fun1和fun2,第一次执行「sayHi()」赋值给fun1的时候,是由隐式绑定转换为了默认绑定,this为window,执行「fun1」的时候,是执行箭头函数,所以箭头函数里的this也是window。
但是对于fun2, 「sayHi.bind(obj)()」中使用bind显示绑定了sayHi的this对象为obj,本来没有bind绑定的时候,是由隐式绑定转变为默认绑定,然后此处我们又强行使用bind绑定回来了。所以「sayHi.bind(obj)()」执行的时候,3处的this为obj,然后「fun2()」执行的时候,4处的this和3处的this一样都是obj。
我们来一个终极题目来综合一下。
var number = 5;
var obj = {
number: 3,
fn: (function () {
var number;
this.number *= 2;
number = number * 2;
number = 3;
return function () {
var num = this.number;
this.number *= 2;
console.log(num);
number *= 3;
console.log(number);
}
})()
}
var myFun = obj.fn;
myFun.call(null);
obj.fn();
console.log(window.number);
我们来分析下这段代码,在obj对象的fn属性定义的时候,就是一个「立即执行函数」,并且带有「闭包」,我们看看这个「立即执行函数」中的this指向了谁?没有new绑定,没有显式绑定,没有隐式绑定,自然就是默认绑定了,this指向了全局变量window。所以「立即执行函数」中的代码可以这样理解。
var number; // number是undefined
window.number *= 2; // 此时全局变量中的number变为 10
number = number * 2; // 由于number是undefined, Number(undefined)为NaN,此处number变为NaN。
number = 3; // 然后又使变量number变为3
执行完fn的立即执行函数后。obj的fn属性是下列这样的:
fn: function() {
var num = this.number;
this.number *= 2;
console.log(num);
number *= 3;
console.log(number);
}
然后我们执行「myFun.call(null)」,这种显式绑定我们在上述文章中说过,如果第一个参数为null,就是转为默认绑定。本例中就是执行obj的fn函数,然后我们再来每一行分析一波:我们首先想想obj的fn函数里面的this是谁?由于是默认绑定所以是window,于是乎。
var num = this.number; // 由于this指向了window,此时window.number为10,则num被赋值为10
this.number *= 2; // 使得全局变量的「number」,变为20
console.log(num); // 输出10
number *= 3; 这个number是obj.fn中的闭包函数中的「number」
console.log(number) ; // 同时输出这个number
改变闭包中的number变量
接着我们执行「obj.fn()」,还是优先执行了obj.fn的立即执行函数。立即执行函数的this还是指向全局变量window,然后依次分析每行代码。立即执行函数中:
var number;
this.number *= 2; // 等价于 window.number *= 2; 使得全局变量的number变为20
number = number * 2; // NaN
number = 3;
然后我们在执行「立即执行函数」返回的函数。此时由于是「obj.fn()」这样调用,所以this指向了obj对象。
var number = this.number; // 3,即obj.number
this.number *= 2; // 使得obj.number变为6
console.log(number); // 输出3
number *= 3; // 此时number还是指闭包中的那个「number」,刚刚number是9,现在就是27了
console.log(number); // 就是输出闭包那个「number」,是27
最后执行「console.log(window.number)」,此时全局变量的number是20,输出20.
参考: