this是javascript中最常用也是比较难理解的一个关键字了,看了很多相关知识,以为理解的差不多了,然后偶然刷到一题又感觉自己不懂了。一直在这种懂与不懂之间徘徊了。
最近在看javascript基础知识,有了一些自己的总结与体会。最开始是看到下面一题:
var age = 100;
var test = {
age: 120,
sayAge: function(){
console.log(this.age)
function go(){
console.log(this.age)
}
go.prototype.age = 60;
return go
}
}
var p = test.sayAge() // ->120
p() // ->100
- 如果将上面的代码最后加上一句new (test.sayAge())()
第一步:test.sayAge() 打印120,返回go
第二步:new go() 打印60 - 如果将go.prototype.age = 60;这句话注释了,执行new (test.sayAge())()
打印结果:120,undefined(this指向new出来的对象实例,不再是window,而this这个对象上没有age这个属性)
之前判断this的指向就是使用口诀:“谁调用它,它就指向谁”,但是有时候这个ta很让人困惑,最后就分不清this指向谁了;有时候又错误的理解this指向它的作用域。比如上面的第二个问题,this没有age,他的全局作用域上有age的,就会错误的认为会打印全局的age。例如下面的代码也常会错误将this=作用域;
var a = 100;
function car(){
console.log(this.a)
}
var myCar = {
car: car
}
myCar.car() //->undefined而不是100
在ES6之前,每个函数的this都是在调用时才被绑定,所以理解函数的调用位置很重要(不是声明位置)
在《你不知道的javascript》上卷中对this的指向总结了4条规则:
-
默认绑定(函数在不带任何修饰情况下进行调用时,此时函数中的this在非严格模式下指向window )
function test(){
bar() //->bar的调用位置
}
function bar(){
foo() // ->foo的调用位置
}
function foo(){
console.log('end')
}
test() // ->test的调用位置
上面调用顺序是test->bar->foo,找到了函数的调用位置,我们来分析它的this绑定,上面的三个函数都是作为独立函数调用的,相当于在全局环境下调用,默认的全局环境中(非严格模式下),this指向window(严格模式下this指向undefined)。
-
隐式绑定(调用位置是否有上下文或者说被某个对象拥有,此时this指向被拥有的对象)
var a = 101;
function foo(){
console.log(this.a)
}
var obj = {
a: 100,
foo: foo
}
obj.foo() // ->100
在上述代码中foo函数作为obj对象foo属性的一个引用来调用,因此可以说obj拥有foo这个函数,所以此时foo函数中的this指向的是obj这个对象。
但是对象引用链只有最后一层会影响调用位置:
obj.obj2.foo()此时foo中的this只会指向obj2
在隐式绑定中最容易弄混的是下面这种情况:
var a = 1
function foo(){
console.log(this.a)
}
var obj = {
a:2,
foo: foo
}
var temp = obj.foo
temp();
在上面的那段代码中有时会错误的理解为最后打印的结果是:2,实际控制台打印的是: 1
在上述代码中的倒数第二步中我们将obj.foo这个引用赋值给temp这个对象,这里相当于将对象的赋值(this是不会被复制过来了,this只有在调用的时候才会被制定)此时就temp这个对象指向foo函数,最后在调用temp()这个函数的时候,可以使用第一条默认绑定规则,此时this就指向window了.
到这里就能明白最上面的那道题中的执行结果了
var p = test.sayAge() // ->120
p() // ->100
第一个使用隐式绑定规则this指向test,第二条中this指向window(声明在全局作用域中的变量就是全局对象的一个同名属性,不是在作用域中查找的),所以window上有age这个属性。
-
显示绑定(使用call、apply、bind,此时this指向前面3个方法所传第一个参数值)
var a = 1
function foo(){
console.log(this.a)
}
var obj = {
a:2,
foo: foo
}
var temp = obj.foo.bind(obj)
temp(); // ->2
// temp.call(obj) ->2
// temp.apply(obj) ->2
这里显示的将temp中this强制绑定到obj这个对象上(bind函数相当于返回一个新函数,不会执行),call/apply会执行函数,两者只是所传的参数不同,在传的参数为null、undefined时this指向window。
-
new绑定
当将一个函数作为构造函数来调用时,使用new来创建一个实例时,会自动执行下面4步
var mycar = new Car()
function Car(){
var this;//创建一个对象
this = {
__proto__: Car.prototype
}
return this
}
- 创建一个全新的对象
- 这个新对象默认有一个__proto__属性指向原型
- 这个对象绑定到函数调用的this
- 如果函数没有返回其他对象,那么返回这个新对象
function Car(color){
this.color = color
}
var mycar = new Car('red')
mycar.color //->red
回到最初的题目
new (test.sayAge())()
执行test.sayAge()时采用隐式绑定规则,此时this指向test这个对象,所以打印120。这个执行结果会返回一个go函数,后面继续执行new go()这时就使用最后一条规则,this指向构造函数go创建的这个实例对象,虽然这个实例对象上没有age这个属性,但是他在创建的时候就从原型上继承了一个age属性。所以在他的原型上找到了,打印60。
后续的注释掉go.prototype.age = 60;这句话,这句导致创建的这个实例对象自己本身没有,原型上也没有,则只能打印undefined。