this

2018-11-26  本文已影响0人  saltfish666

this可以说是JS最复杂、网上讨论最多的一个话题了。我希望我对this的理解能够帮助到你。

为什么有this

其实,在python等语言中,是没有this的。

class Person:
    def __init__(self, birthyear):
        self.birthyear = birthyear
    
    def logAge(self):
        print(2018 - self.birthyear) 

p = Person(1999)
p.logAge()

但是在进行类声明的时候,为了获得对即将创建的对象的引用,python设计者强制规定了类中的方法的第一个参数是 这个对象的引用。这样做很符合python的设计哲学:明确而不隐性。

但是这样做的代价是很容易让代码变得冗长。JS的设计者选择了使用this以使得编程语言更加灵活。

如何判断this?

我把判断this分为这几种情况

基本的,除了强制绑定和严格模式下的一个特例(下面会说到),this一定是指代一个对象!关键是找到这个对象究竟是什么。

构造函数的this绑定

正如上述所言,this最初的用途是在构造函数中指代即将创建的对象。

function Person(name, birthyear){
    this.name = name
    this.age = 2018 - birthyear
}

var p = new Person('lee', 1999)
console.log(p) // Person { name: 'lee', age: 19 }

这就是this的基本用法,在使用new关键字的时候,构造函数中this就是指代即将创建的对象。我们也可以看到有了这种隐性的规定后,确实比python简洁。

隐性绑定

看下面的代码:
代码一:

function foo(){
    console.log(this.a)
}

var a = 2 
foo()

代码二:

function foo() { 
  console.log( this.a );
}
var a = 1
var obj = { 
  a: 2,
  foo: foo 
}
obj.foo()
var bar = obj.foo
bar()

结果是多少呢?

ES规范:

说人话

this只可能存在于全局作用域或者一个函数中

this存在于全局作用域

参考这种情况

var a = 2 
console.log(this.a)

这是最基本的情况,没什么好说的,记住结论就行:这时this就是指代全局变量。在node中运行结果是undefined,在浏览器中结果是2。因为在浏览器情况下,全局声明的变量会被挂载到全局对象window上,所以this.a的结果是2,而在node中不会。(一个无关痛痒的知识点,有的时候也挺烦人的啊。)

不过有些人认为这种情况下将this绑定为全局对象不合理。作为妥协,在严格模式下,上述this不再指向全局变量,而变成了undefined,所以执行上述代码会报错。

this存在于一个函数中

而在JS中,所有的函数都是对象,而对象都是引用类型,所以函数都是引用类型。

而一个函数可以分为两个部分:函数名和函数体。比如function foo(){console.log(this.a)},就可以分为两个部分:函数名foo和函数体function(){console.log(this.a)}

要执行一个函数,肯定要找到函数的地址,用C语言的话来说,那就肯定要用到一个指向函数地址的指针啊!而这个指针挂载到哪个对象上,this就会指代这个对象。

上述代码一中:
函数名foo指向了函数体 function(){console.log(this.a)}而其实foo并没有挂载到任何对象上,如果出现这种情况: 函数体 function(){console.log(this.a)}中的this会被设定为全局对象!!所以上述代码一在浏览器下打印出2,node下打印出undefined。同样的,在严格模式下,this依旧是undefined

参考这样的代码:

var a = 1
function foo() { 
    var a = 2
    function bar() {
        console.log(this.a)
    }
    bar()
}
foo()

真正被执行的代码是函数体function(){console.log(this.a)},而找到这个函数体的是变量(或者称它为函数指针)barbar并没有挂载到任何对象上,所以它和上述代码一中的情况一样,这个函数体的this也是全局变量!
再夸张一点:

var a = 1
function foo() { 
    var a = 2
    function bar() {
        var a = 3
        function biu() {
            console.log(this.a)
        }
        biu()
    }
    bar()
}
foo()

虽然函数体的嵌套很多,但是结果一样,this依旧指向全局对象!

接下来讨论代码二,是重点。为了读者的观看体验,我把代码二再重复的贴一遍。
代码二:

function foo() { 
  console.log( this.a );
}
var a = 1
var obj = { 
  a: 2,
  foo: foo 
}
obj.foo()
var bar = obj.foo
bar()

无论是倒数第三行,还是倒数第一行,最终执行的函数体都是 function(){console.log(this.a)}
执行倒数第三行的时候,是obj.foo这个'指针变量' 指向了这个函数体,而obj.foo是挂载到obj上的。所以说这个函数体中的this就是obj,所以最终会打印出2

而执行到倒数第一行时,是bar这个'指针变量'指向了这个函数体,所以此时this就是全局变量,最终结果会符合我们上面说的情况。

有的同学可能觉得我这种判断this的方法比较麻烦,觉得自己这种方法不是很好。那我们再来加一点难度。

强制绑定

ES提供了一种给函数强制绑定this的方法,即:

强制绑定的优先级高于上面三种,所以被称为强制绑定

Function.prototype.bind ( thisArg, ...args )

根据ES标准,function.bind(thisArg) 会返回一个新的函数funcObj,这个新的函数有一个内置的属性 funcObj.[[BoundThis]],它的值为thisArg 。这个属性对JS程序员不可见,属于JS引擎层面考虑的事。而以后调用这个函数的时候,函数体内部的this就会强制绑定为thisArg了。这种强制绑定的优先级要高于我们之前说的

function foo() { 
    console.log( this.a );
}
var a = 1
var obj = { 
    a: 2,
    foo: foo 
}
var obj2 = {
    a: 3
}
obj.foo() // 2

bindFoo = obj.foo.bind(obj2)
bindFoo() // 3

var bar = bindFoo
bar() // 3

执行到obj.foo()的时候,执行的函数体是function(){console.log(this.a)}, 按照我们之前说的隐式绑定,this指向obj,将会打印2
然后执行 bindFoo = obj.foo.bind(obj2),这时候其实返回了一个新的函数对象,我们把它叫做funcObj把。函数对象funcObj的属性 funcObj.[[BoundThis]]的值被设定为 obj3
接下来执行bindFoo(),其实就是执行funcObj()。而它的this已经固定,不再按照隐式绑定的规则查找,所以会打印3
接下来执行var bar = bindFoo。这时,bar其实指向了funcObj,所以结果依旧是打印3

Function.prototype.call ( thisArg, ...args )

这个函数将会在程序执行时,将函数体内的this强行绑定为 thisArg。它和 Function.prototype.apply ( thisArg, argArray ) 的唯一区别在于:后者的参数写成数组的形式。

function foo(x, y) { 
    console.log( this.a + x + y);
}
var a = 1
var obj = { 
    a: 2,
    foo: foo 
}
var obj2 = {
    a: 3
}

foo.apply(obj, [0, 0]) // 2
obj.foo.call(obj2, 0, 0) // 3

这几个this绑定的优先级

写几段代码测试即可

var obj1 = {
    a: 1
}
var obj2 = {
    a: 2
}

function foo() {
    console.log(this.a)
}
var bindFoo = foo.bind(obj1)
bindFoo.call(obj2)

最终结果是打印出1 1,所以.bind绑定强于.call .apply.


var obj1 = {
    a: 1
}

function foo() {
    this.a = 3
}
var bindFoo = foo.bind(obj1)

var obj = new foo()
console.log(obj.a)  // 3
console.log(obj1.a) // 1

上述结果可以看到使用new关键字的时候,构造函数中的this就是指向新创建的对象,而不在乎之前可能存在的.bind()绑定。

总结

判断this的时候按照以下步骤按顺序来

  1. 当this存在于全局作用域时,this指向全局对象,严格模式下this为undefined
  2. 当一个函数作为构造函数时,函数体中的this执行新创建的对象
  3. 如果一个函数被通过.bind(thisArg)来强制绑定,以后该函数内部的this总是指向thisArg
  4. 如果一个函数在执行过程中被通过.call(thisArg) .apply(thisArg)绑定,那么这次执行过程中this指向thisArg
  5. 在隐式绑定中,可以将这个函数分为两个部分:函数名和函数体。
    • 函数名挂载到哪个对象上,函数体中的this就指向这个对象
    • 如果函数名没有挂载到任何对象上,函数体中的this就指向全局对象,但是在严格模式下this为undefined

实战

// 代码节选自《你不知道的JS(上)》
function foo() { 
    console.log( this.a );
}
function doFoo(fn) {
    fn()
}
var obj = { 
    a: 2,
    foo: foo 
};
var a = "oops, global"
doFoo( obj.foo )  // 打印多少?

其实很好理解,最终执行的函数体是 function(){console.log(this.a)},指向这个函数体的是fn,而fn没有挂载在任何对象上,就会被默认为全局对象,在浏览器中打印oops, global,在node中打印undefined

再来看看

// 代码节选自《你不知道的JS(上)》
function foo() { 
    console.log( this.a )
 }

 var obj = { 
    a: 2,
    foo: foo 
}
var a = "oops, global"
setTimeout( obj.foo, 100 )

这和上述情况一模一样,只是显得隐蔽一点。

上一篇下一篇

猜你喜欢

热点阅读