彻底搞懂this

105 阅读6分钟

this 到底是什么

因为 this 的取值是函数执行上下文(context)的一部分,每次调用函数,都会产生一个新的执行上下文环境。当代码中使用了 this ,这个 this 的值就直接从执行上下文中获取了,而不会从作用域链中搜寻。

this 是在运行时进行绑定的,并不是在编写时绑定,它的上下文取决于函数调用时的各种条件。this 的绑定和函数声明的位置没有任何关系,只取决于函数的调用方式。当一个函数被调用时,会创建一个活动记录(也称为执行上下文)。这个记录会包含函数在哪里被调用、函数的调用方法、传入的参数等信息。this 就是记录的其中一个属性,会在函数执行的过程中用到。

this 在全局作用域

浏览器环境下 this 在全局作用域指向 window
Node环境下 this 在全局作用域指向空对象 {},Node 中每一个文件就是一个模型,经过加载、编译,会将代码放到一个函数中,通过 call() 调用该函数。而 call() 传入的第一个参数(用于指定 this )是一个 {}

this 的绑定规则

默认绑定

独立函数调用,是无法应用其他规则时的默认规则。
声明在全局作用域中的变量就是全局对象的一个同名属性。它们本质上是一个东西,并不是通过赋值得到的。如果函数是直接使用不带任何修饰的函数引用进行调用的,只能使用默认绑定,无法应用其他规则。this 的绑定规则完全取决于调用位置,但是只有函数运行在非严格模式下时,默认绑定才能绑定到全局对象。

隐式绑定

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

无论是直接在 obj 中定义还是先定义再添加为引用属性,这个函数严格来说都不属于 obj 对象。调用位置会使用 obj 上下文来引用函数,当 foo 被调用时,它的落脚点指向 obj 对象。当函数引用有上下文对象时,隐式绑定规则会把函数调用中的 this 绑定到这个上下文对象。

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

var obj2 = {
  a: 42,
  foo: foo
}

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

obj1.obj2.foo(); // 42

显式绑定

允许指定函数调用时的 this 值,这意味着可以将任意函数作为任意对象的方法来调用。即使这个函数实际上并不是该对象的方法。 call() 和 apply() 的第一个参数是一个对象,它们会把这个对象绑定到 this ,接着在调用函数时指定这个 this 。因为可以直接指定 this 绑定的对象,因此称之为显式绑定。

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

var obj = {
  a:2
}

foo.call(obj) // 2

通过 foo.call(..) 可以在调用 foo 时强制把它的 this 绑定到 obj 上。
bind() 硬绑定是显式绑定的一个变种。bind() 会返回一个硬编码的新函数,它会把参数设置为 this 的上下文并调用原始函数。

function foo(something) {
  console.log( this.a, something )
  return this.a + something
}

var obj = {
  a:2
}
var bar = foo.bind( obj )
bar( 3 )  // 2 3

注意:bar() 是独立函数调用,为什么 this 不绑定 window ?因为 bind 是显式绑定比隐式绑定优先级高。

new 绑定

使用 new 来调用函数,或者说发生构造函数调用时,会自动执行下面的操作。

  1. 创建或者说构造一个全新的对象;
  2. 这个新对象会被执行 [[原型]] 连接;
  3. 这个新对象会被绑定到函数调用的 this
  4. 如果函数没有返回其他对象,那么 new 表达式中的函数调用自动返回这个新对象。
function foo(a) {
  this.a = a
}

var bar = new foo(2)
console.log( bar.a )  // 2

this 绑定的优先级

new 绑定 > 显式绑定 > 隐式绑定 > 默认绑定。根据优先级来判断函数在某个调用位置应用哪条规则。可以按照下面顺序进行判断。

  1. 函数是否在 new 中调用?如果是,this 绑定的是新创建的对象。

    var bar = new foo()
    
  2. 函数是否通过 call apply 显式绑定或 bind 硬绑定调用?如果是,this 绑定的是指定的对象。

    var bar = foo.call(obj)
    
  3. 函数是否在某个上下文对象中调用(隐式绑定)?如果是,this 绑定的是那个上下文对象。

    var bar = obj.foo()
    
  4. 如果以上都不是,使用默认绑定。非严格模式下绑定到全局对象,严格模式下绑定到 undefined

    var bar = foo()
    

箭头函数的 this

箭头函数中的 this 与定义时所处的上下文绑定,且不能被改变。箭头函数 this 指向取决于它外层找到的离它最近的第一个非箭头函数的 this

显式绑定 this 的方式

call()

function.call(thisArg, arg1, arg2, ...) thisArg 可选的。在 function 函数运行时 this 要指向的那个对象。如果这个函数处于非严格模式下,则指定为 null 或 undefined 时会自动替换为指向全局对象。

// 将类数组对象转为数组
Array.prototype.slice.call(arguments);

// 取出数组中最小数值
var arr = [1,2,3]
Math.min.call(this,...arr) // 1

apply()

A.apply( B, [ arg1, arg2, ... ] ) B是调用方法的真正对象。apply() 第一个参数也是 this 要指向的那个对象,如果设为 nullundefined,则等同于指定全局对象。
第二个参数是一个数组,数组的成员会依次作为参数传入原函数。

// 转换类数组对象
Array.prototype.slice.apply({0: 1,1: 2, length: 2}) //[1,2]

// 求数组元素中最大数
Math.max.apply(null,[1, 2, 3, 4, 5])

// 将一个数组添加到另一个数组尾部
var arr1 = [0,1,2]
var arr2 = [3,4,5]
// arr1调用push方法,参数是arr2提供
Array.prototype.push.apply(arr1, arr2)
console.log(arr1) //[ 0, 1, 2, 3, 4, 5 ]

bind()

如果我们希望一个函数总是显式的绑定到一个对象上,bind() 方法会创建一个新的函数实例,其 this 会被绑定到传给 bind() 的对象。bind() 可以将原始函数和新函数的参数做合并,在原始函数中输出。

function foo(num1,num2,...args) {
  console.log(num1, num2,...args) 
}

var bar = foo.bind(null,10,20)
bar(30,40) // 10 20 30 40

sayColor() 上调用 bind() 并传入对象 o 创建了一个新函数 objectSayColor()objectSayColor() 中的 this 值被设置为 o,因此直接调用这个函数,即使是在全局作用域中调用,也会返回字符串 'blue'

window.color = 'red'
var o = {
  color: 'blue'
}
function sayColor() {
  console.log(this.color)
}

let objectSayColor = sayColor.bind(o)

objectSayColor() // blue

注意: bind() 并不会改变原有函数的 this 指向,而是创建新的函数,改变的是新函数的 this