Skip to content

Latest commit

 

History

History
395 lines (277 loc) · 8.98 KB

第二章 this 全面解析.md

File metadata and controls

395 lines (277 loc) · 8.98 KB

第二章 this 全面解析

基本概念

this 是个很特别的关键字, 被自动定义在所有函数的作用域中;

this 关键字并不会像它的字面意思那样, 指向函数自身; 相对词法作用域(只关注函数在何处声明), 它的行为更像动态作用域(只关注函数从何处调用);

下面一个简单例子, 解释词法作用域动态作用域的区别:

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

function bar() {
    var a = 2
    foo()
}

var a = 1

bar()

运用词法作用域:

  • foo 函数在全局作用域下声明, 所以其作用域内部可以访问到全局作用域的 a, 但它访问不到 bar 函数内部的 a;
  • 输出结果 1

运用动态作用域:

  • foo 函数在 bar 函数内部调用, 第一个查找到的 a 变量是 bar 函数作用域内部的 a;
  • 输出结果 2

绑定的四大规则

this 的指向, 是根据函数调用的位置 决定的, 我们必须先找到调用位置, 然后判断其适用以下四条规则的哪一条

默认绑定

我们可以把这条规则看做无法使用其他规则时的默认规则;

直接使用不带任何修饰的函数引用, 在调用时, 只能使用 默认绑定;

默认绑定 会将 this 绑定到 window 对象上;(this === window)

function foo() {
  console.log(this === window)

  bar() // true
}

function bar() {
  console.log(this === window)
}

foo() // true

但在严格模式下, 默认绑定会绑定一个 undefined:

"use strict"

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

foo() // undefined

隐式绑定

通过考虑 调用位置是否具有上下文对象, 或者说是否被某个对象拥有包含, 来判断是否运用这条规则:

如下示例, 我们声明一个 foo 函数, 并将其赋值给 obj 对象的 foo 属性:

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

var obj = {
    a: 1,
    foo: foo
}

obj.foo() // 1
foo()     // undefined

对象属性引用链中只有最后一层在调用位置中起作用, 如下:

function fn() {
    console.log(this.msg)
}

var child = {
    msg: 'child',
    fn: fn
}

var parent = {
    msg: 'parent',
    child: child
}

parent.child.fn() // child

隐式丢失

如下示例, o.foothis 隐式绑定在了 o 对象上, 而 bar 引用了 o.foo 函数本身, 所以此时的 bar() 其实是一个不带任何修饰的函数调用, 因此使用了 默认绑定 规则:

var o = {
    a: 1,
    foo() {
        console.log(this.a)
    }
}

var bar = o.foo

o.foo() // 1
bar()   // undefined

另一个很出乎意料的例子, 示例中, bar(o.foo) 实际上采用了隐式赋值: callback = o.foo, 事实上跟上面的例子一样, 都是直接引用了 o.foo 函数本身, 所以造成了 隐式丢失:

function bar(callback) {
    callback()
}

var o = {
    a: 1,
    foo() {
        console.log(this.a)
    }
}

bar(o.foo) // undefined

显式绑定

现在, 我们并不想通过 隐式 或者 默认 的方式来间接绑定 this 的指向, 我们需要强制的为函数指定一个绑定对象!

我们可以通过使用 call()apply() 方法来实现;

JavaScript 提供的大多数函数以及我们自己创建的所有函数都可以使用这两个方法;

fn.call(obj Object [, ...arguments])

第一个参数接收一个对象, 作为 this 关键字绑定的对象, 第二个参数是该函数传递的参数;

function foo(a, b) {
    console.log((a + b) * this.c)
}

var obj = {
    c: 2
}

foo.call(obj, 1, 2) // 6

// foo.apply(obj, [1, 2])
// call() 与 apply() 的效果完全一致, 唯一不同的只是传递参数的格式不同

硬绑定

使用显示绑定时, 我们重复进行绑定, 仍然会让之前的绑定值丢失:

var obj = { name: 'muzi' }

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

foo.call(obj)  // muzi

setTimeout(() => (
    foo.call({ name: 'yaya' } // yaya
)), 100)

如果我们想避免这种情况, 就需要使用 硬绑定;

硬绑定的典型应用场景就是创建一个包裹函数, 传入所有的参数并返回接收到的所有值:

var obj = {
    name: 'muzi'
}v

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

function wrapper() {
    foo.call(obj)
}

wrapper()  // muzi
wrapper.call({ name: 'yaya' })  // muzi

虽然 wrapper 绑定了一个新的对象, 但当 wrapper 每次被调用时, foo 都会显示绑定 obj 对象;

所以无论 wrapper 如何调用, foo 函数的绑定值都不会被改变.

我们可以根据这个特性, 创建一个辅助绑定函数:

function bind(fn, obj) {
  return function() {
    return fn.apply(obj, arguments)
  }
}
function foo(b, c) {
    return this.a + b + c
}

var bar = bind(foo, { a: 1 })

bar(2, 3) // 6
bar.call({ a: 10 }, 2, 3) // 6

事实上, 早在 ES5 就提供了原生的硬绑定方法: Function.prototype.bind

var bar = foo.bind({ a: 1 })

bar(2, 3)  // 6
bar.call({ a: 10 }, 2, 3)  // 6

Function.prototype.bind 的实现:

Function.prototype._bind = function(obj) {
  var self = this
  return function() {
    return self.apply(obj, arguments)
  }
}

new 绑定

构造函数只是一些使用 new 操作符时被调用的函数, 它们并不会属于某个类, 也不会实例化一个类, 它们只是被 new 操作符调用的普通函数而已

普通函数

以下示例, Person 是一个所谓的构造函数, 根据默认绑定的规则, Person 被调用时, 内部的 this 应当指向全局作用域;

因此, 当我们访问 person.name 时, 会得到 TypeError 的结果, 因为 Person() 没有返回任何东西, 是个 undefined

function Person() {
  this.name = 'muzi'
}

const person = Person()

console.log(window.name) // muzi
console.log(person.name) // TypeError

可以看到, 没有被 new 操作符调用的所谓构造函数, 仅仅是普通函数而已

构造函数

const person = new Person()

console.log(window.name)  // undefined
console.log(person.name)  // muzi

我们会得到截然不同的结果, 这是因为 new 操作符做了以下四件事情:

  • 创建或构造了一个全新的对象
  • 这个新对象会被执行[[原型]]连接
  • 函数中的 this 会指向这个新对象
  • 如果被调用的函数没有返回(return), 则 new 表达式中的函数调用会自动返回这个新对象 (对没错, 就是普通的对象, 带花括号的那种!)

优先级

new > 显式绑定 > 隐式绑定 > 默认绑定

ES6 箭头函数

箭头函数不使用 this 的四种标准规则, 而是根据词法作用域来决定 this

var obj = { a: 1 }

// 1
function fn() {
    setTimeout(function() {
        console.log(this.a)
    }, 1000)
}

fn.call(obj)  // undefined

// 2
function arrow() {
    setTimeout(() => {
        console.log(this.a)
    }, 1000)
}

arrow.call(obj) // 1

代码片段1:

fnthis 指向 obj, setTimeout 的回调函数是个普通函数, 并且在没有任何修饰的情况下引用, 执行 默认绑定 规则, 其 this 指向全局作用域

这种情况下, 通常有两种方法, 可以让回调函数绑定到父级的 this

var obj = { a: 1 }

function fn() {
    var self = this
    setTimeout(function() {
        console.log(self.a)
    }, 1000)
}

fn.call(obj) // 1
var obj = { a: 1 }

function fn() {
    setTimeout(function() {
        console.log(this.a)
    }.bind(this), 1000)
}

fn.call(obj) // 1

代码片段2:

arrowthis 指向 obj, setTimeout 的回调函数是个箭头函数, 根据 词法作用域, 该箭头函数的 this 也指向 obj

小结

如果要判断一个运行中函数的 this 绑定, 就需要找到这个函数的直接调用位置. 找到之后 就可以顺序应用下面这四条规则来判断 this 的绑定对象:

  • new 调用? 绑定到新创建的对象
  • call apply(或 bind) 调用? 绑定到指定对象
  • 由 上下文对象(context) 对象调用? 绑定到那个上下文对象
  • 默认: 在严格模式下绑定到 undefined, 否则绑定到全局对象