本文对JavaScript中的this关键字进行全方位的解析,看完本篇文章,但愿读者们可以彻底理解this的绑定问题。app
开篇:对于那些没有投入时间去学习this机制的JavaScript开发者来讲,this的绑定是一件使人困惑的事。(包括曾经的本身)。函数
误区:学习this的第一步是明白this既不指向函数自己也不指向函数的词法做用域,你是否被相似这样的解释所误导?但其实这种说法都是错误的。学习
归纳:this实际是在函数被调用时发生的绑定,它所指向的位置彻底取决于函数被调用的位置。this
在理解this的绑定过程以前,首先要理解调用位置:调用位置就是函数在代码中被调用的位置(而不是声明的位置)。code
因此说,寻找调用位置就是寻找“函数被调用的位置”,这里最重要的点是要分析调用栈(存放当前正在执行的函数的位置)。对象
什么是调用栈和调用位置?排序
关系:调用位置就在当前正在执行的函数(调用栈)的前一个位置。继承
function func1() { // 当前调用栈:func1 // 当前调用位置是全局做用域(调用栈的前一个位置) console.log('func1') func2() // 这里是:func2的调用位置 } function func2() { // 当前调用栈:func1 -> func2 // 当前调用位置是在func1(调用栈的前一个位置) console.log('func2') func3() // 这里是:func3的调用位置 } function func3() { // 当前调用栈:func1 -> func2 -> func3 // 当前调用位置是在func2(调用栈的前一个位置) console.log('func3') } func1() // 这里是:func1的调用位置
关注点:咱们是如何从调用栈中分析出真正的调用位置的,由于这决定了this的绑定。ip
最经常使用的函数调用类型:独立函数调用作用域
function getName() { console.log(this.name) } var name = 'kyrie' getName() // 'kyrie'
当调用getName()时,this.name拿到了全局对象的name。由于getName()是直接调用的,不带任何修饰符,使用的是默认绑定,所以this指向全局对象(非严格模式)。
若是使用严格模式('strict mode')呢?
function getName() { 'use strict'; console.log(this.name) } var name = 'kyrie' getName() // 'TypeError: this is undefined'
那么全局对象没法使用默认绑定,所以this会绑定到undefined。
调用位置是否有上下文对象
function getName() { console.log(this.name) } var person = { name: 'kyrie', getName: getName } person.getName() // 'kyrie'
当getName()被调用时,它的落脚点指向person对象,当函数引用有上下文对象时,隐式绑定会把函数调用中的this绑定到这个上下文对象,所以调用getName()时this被绑定到person,所以this.name跟person.name是同样的
常见问题:隐式丢失?
function getName() { console.log(this.name) } var person = { name: 'kyrie', getName: getName } var getName2 = person.getName() // 函数别名 var name = 'wen' // name是全局对象的属性 getName2() // 'wen' 这里拿到的是全局对象的name
解释:虽然getName2是person.getName的一个函数引用,但它引用的getName函数的自己,所以getName2()调用时不带任何修饰符,使用的是默认绑定,所以this绑定了全局对象。
使用call() / apply() / bind() 指定this的绑定对象
function getName() { console.log(this.name) } var person = { name: 'kyrie' } getName.call(person) // 'kyrie' getName.apply(person) // 'kyrie'
经过getName.call()/ getName.apply() 调用强制把它的this绑定到person上。
全部函数均可以用new来调用,这种函数调用称为构造函数调用。
重点:实际上并不存在所谓的“构造函数”,只有对于函数的“构造调用”。
function setName(name) { this.name = name } var person = new setName('kyrie') console.log(person.name) // 'kyrie'
使用new调用setName()时,会建立一个新对象并把这个新对象绑定到setName()调用的this上,并把这个对象返回。
毫无疑问,默认绑定的优先级是四条规则中最低的,因此暂不考虑它。
function getName() { console.log(this.name) } var p1 = { name: 'kyrie', getName: getName } var p2 = { name: 'wen', getName: getName } p1.getName() // 'kyrie' p2.getName() // 'wen' p1.getName.call(p2) // 'wen' p2.getName.call(p1) // 'kyrie'
结果,显式绑定的优先级比隐式绑定高。
function setName(name) { this.name = name } var p1 = { setName: setName } var p2 = {} p1.setName('kyrie') console.log(p1.name) // 'kyrie' p1.setName.call(p2, 'wen') console.log(p2.name) // 'wen' var p3 = new p1.setName('zbw') console.log(p1.name) // 'kyrie' console.log(p3.name) // 'zbw'
结果,new绑定的优先级比隐式绑定高
function setName(name) { this.name = name } var p1 = {} // bind会返回一个新的函数 var setP1Name = setName.bind(p1) setP1Name('kyrie') console.log(p1.name) // 'kyrie' var p2 = new setP1Name('wen') console.log(p1.name) // 'kyrie' console.log(p2.name) // 'wen'
结果,new绑定的优先级比显示绑定高
综上,优先级的正确排序:
从高到低: new > 显示 > 隐式 > 默认
如今咱们能够根据优先级来判断函数在某个位置调用this的指向。
var p1 = new Person()
var p1 = setName.call(p2)
var p2 = p1.setName()
var p1 = setName()
以上上提到判断this指向的四条规则包含全部正常的函数,除了ES6中的箭头函数。
归纳:箭头函数不像普通函数那样使用function关键字定义,而是用 “胖箭头” => 定义 。并且箭头函数并不适用以上的四条规则,它的this绑定彻底是根据 外层做用域(函数或者全局) 来决定的。
function getName() { // 箭头函数的this指向外层做用域 return (name) => { console.log(this.name) } } var p1 = { name: 'kyrie' } var p2 = { name: 'wen' } var func = getName.call(p1) func.call(p2) // 'kyrie'
getName()内部建立的箭头函数会捕获调用时外层做用域(getName)的this,因为getName的this经过显示绑定到p1上,因此getName里建立的箭头函数也会指向p1,最重要的一点:箭头函数的this没法被修改(即便是优先级最高的new绑定也不行)
要判断一个运行中的函数的this绑定,须要找到该函数的调用位置(结合调用栈),接着根据优先级得出的四条规则来判断this的绑定对象。
ES6的箭头函数不适用以上四条规则,而是根据当前的词法做用域来决定this绑定,也就是说,箭头函数会继承外层函数调用的this绑定(不管绑定到什么),并且箭头函数的this绑定没法被修改。