理性分析 JavaScript 中的 this

在每个方法中,关键字 this 表示隐式参数。 —— 《Java 核心技术 卷Ⅰ》javascript


this 是什么?

了解 python 的同窗可能会知道,python 构造函数中老是会出现 self 参数。这个参数用来表示建立的实例对象。java

class Student(object):
    def __init__(self, name, score):
        self.name = name
        self.score = score
复制代码

在 JavaScript 和 Java 中这个参数被隐藏了。咱们没必要在参数列表中显式声明这个参数,就能够在函数中使用这个参数。这个参数就是 this 。python

function Student(name, score){
	this.name = name
	this.score = score
}
var studentA = new Student('a', 100)
console.log(studentA.name, studentA.score) // a 100
复制代码

隐式参数

援引 《Java 核心技术 卷Ⅰ》 中的一句话:在每个方法中,关键字 this 表示隐式参数。 所谓的隐式参数,就是没有在参数列表中显式声明的参数。隐式参数和参数列表中定义的显式参数统称为形式参数。与形式参数相对应的是实际参数。bash

形式参数和实际参数

形式参数,简称形参。形参就是在定义函数的时候使用的参数,用来接收调用该函数时传递的参数。如上述代码中的 name ,score 参数都是形参。闭包

实际参数,简称实参,实参就是调用该函数时传递的参数。如上述代码中的 'a' , 100 都是实参。app

为何 this 的值是在调用时肯定的?

《 你不知道的JavaScript(上卷)》中提了一个问题,问:为何采用词法做用域的 JavaScript 中的 this 的值是在调用时肯定的?函数

在理解了形参和实参以后,咱们便能很好地理解这个问题了。oop

由于 this 是一个形参,形参的值是由实参决定的。而传参这个操做时在调用时发生的,因此 this 的值是在调用时肯定的。ui


this 的值

既然 this 的值是由实参的值决定的,那么这个实参的值究竟是什么呢?this

参考 《Java 核心技术 卷Ⅰ》 中的一句话:隐式参数的值是出如今函数名以前的对象。看成为构造函数时,this 用来表示建立的实例对象。来看两个例子:

function bar () {
  console.log(this.name)
}
var foo = {
  name: 'foo',
  bar: bar
}
foo.bar() // foo
复制代码

this 指向函数名(bar)以前的 foo 对象

function Student(name, score){
	this.name = name
	this.score = score
}
var studentA = new Student('a', 100)
console.log(studentA.name, studentA.score) // a 100
复制代码

this 指向建立的实例对象 studentA

call apply bind

JavaScript 也提供了几个函数去改变 this 的值。这几个函数都会返回一个原函数的拷贝,并在这个拷贝上传递 this 的值。因此从结果上看,咱们能够看到原有的 this 会被覆盖。

function bar () {
  console.log(this.name)
}
var foo = {
  name: 'foo',
  bar: bar
}
var obj = {
  name: 'obj',
}
foo.bar.call(obj) // obj
复制代码

this 指向新的对象 obj 。

为何 this 指向了全局对象?

《 你不知道的JavaScript(上卷)》中描述了一种现象:this 丢失了原来的绑定对象,指向了全局对象。书中称为隐式丢失。来看示例:

function foo() { 
	console.log( this.a )
}
var obj = { 
	a: 2,
	foo: foo 
}
var bar = obj.foo // 赋值操做
var a = "oops, global" 
bar() // "oops, global"
复制代码

JavaScript 只有值传递,没有引用传递。在赋值操做的时候,实际上是将一个引用的拷贝赋值给另一个变量。var bar = obj.foo 在这个语句中,没有传参操做,因此 this 的值是由 bar 函数在调用时传递的那个实参决定的。这个实参如未显式指定,那么即是指向全局对象。因此上述代码中的 this 指向了全局对象。

一样的,咱们在函数传参的过程当中,常常发现隐式丢失问题,缘由也是中间发生了一次赋值操做。代码示例以下:

var name = 'global'
function bar () {
  console.log(this.name)
}
var foo = {
  name: 'foo',
  bar: bar
}
function callFunc(func){
  func()
}
callFunc(foo.bar) // global
复制代码

在传参的过程当中,发生了func = foo.bar的赋值操做,致使最后 this 的值指向了全局对象。

可是若是咱们使用 bind 绑定了 this 的值,那么在发生赋值操做时,this 的值将再也不改变。来看下面例子。

再谈 bind

bind 和 call ,apply 有一点不一样的是 call,apply 返回的是调用结果,而 bind 返回的是绑定 this 后的函数对象。那么当绑定 this 后的函数做为实参传入函数时,与未绑定 this 的结果就彻底不一样了。

来看下面的例子。

var name = 'global'
function bar () {
  console.log(this.name)
}
var foo = {
  name: 'foo',
  bar: bar
}
function callFunc(func){
  func()
}
callFunc(foo.bar.bind(foo)) // foo
复制代码

将 bar 函数中的 this 绑定到 foo 再传入 callFunc 函数中,最后打印的结果是 foo 。

实际上, bind 函数内部维护了一个闭包,使得调用始终发生在函数内部,来保证 this 的值不变。来看 MDN 提供的 ployfill

if (!Function.prototype.bind) {
  Function.prototype.bind = function(oThis) {
    if (typeof this !== 'function') {
      // closest thing possible to the ECMAScript 5
      // internal IsCallable function
      throw new TypeError('Function.prototype.bind - what is trying to be bound is not callable')
    }

    var aArgs   = Array.prototype.slice.call(arguments, 1),
        fToBind = this,
        fNOP    = function() {},
        fBound  = function() {
          return fToBind.apply(this instanceof fNOP
                 ? this
                 : oThis,
                 aArgs.concat(Array.prototype.slice.call(arguments)))
        };

    if (this.prototype) {
      // Function.prototype doesn't have a prototype property
      fNOP.prototype = this.prototype
    }
    fBound.prototype = new fNOP()

    return fBound
  }
}
复制代码
// return 部分
 return fToBind.apply(this instanceof fNOP
                 ? this
                 : oThis,
                 aArgs.concat(Array.prototype.slice.call(arguments)))
复制代码

在 return 的时候使用了 apply 函数来改变 this ,若未发生 new 操做,那么这个 this 的值将绑定到 bind 函数提供的那个对象。

new 操做

当发生 new 操做时,this 将绑定到这个实例对象。 从上面这个 ploy fill 能够看出 new 操做中的 this 值会覆盖原有 this 的值。来看例子

function bar () {
  this.name = 'bar'
}
var foo = {
  name: 'foo',
}

var a = bar.bind(foo)
a()
console.log(foo.name) // bar
var b = new a()
console.log(b.name) // bar
复制代码

当执行 new 操做以前,a 函数中的 this 指向 foo。当执行 new 操做以后,a 函数中的 this 指向了 b 。

new 操做会返回一个从新绑定 this 后的新对象。因此当发生 new 操做以后,原有的 this 发生了改变。具体步骤以下:

  1. 建立(或者说构造)一个全新的对象。
  2. 这个新对象会被执行 [[ 原型 ]] 链接。
  3. 这个新对象会绑定到函数调用的 this 。
  4. 若是函数没有返回其余对象,那么 new 表达式中的函数调用会自动返回这个新对象。

箭头函数中的 this

箭头函数中的 this 继承了父做用域的 this。

var name = 'global'
var foo = {
  name: 'foo',
  bar: () => {
	console.log(this.name)
  }
}

foo.bar() // global
复制代码

箭头函数的父做用域为全局做用域,全局做用域的 this 指向全局对象,因此 this 指向了全局对象。

var name = 'global'
var foo = {
  name: 'foo',
  bar: function () {
	setTimeout(() => {
	  console.log(this.name)
	},100)
  }
}

foo.bar() // foo
复制代码

箭头函数的父做用域为 bar 函数,在调用时,父做用域 bar 函数中的 this 指向了 foo 函数,因此箭头函数中的 this 指向了 foo 。

严格模式下的 this

严格模式下禁止 this 指向全局对象。在严格模式下当 this 指向全局对象的时候会变成 undefined 。


总结

  1. this 指向建立的实例对象或函数名以前的对象。如未指定,即是指向全局对象。
  2. 因为 call 、apply 、bind 函数会返回一个原函数的拷贝,并在这个拷贝上传递 this 值。因此当使用 call 、apply 、bind 函数会覆盖原有的 this 值。
  3. new 操做能够覆盖 call、apply、bind 绑定的 this 值。

tips

  • 严格模式下禁止 this 指向全局对象。在严格模式下当 this 指向全局对象的时候会变成 undefined 。
  • 在发生赋值操做时,因为引用复制, this 的值指向被赋值变量的调用对象。
  • ES 6 中新增箭头函数,能够继承父做用域的 this ,能够解决 this 隐式丢失的问题。

相关知识点

  • 词法做用域和动态做用域
  • 闭包
  • 做用域和做用域链
  • 严格模式
  • ES6 新增特性
  • 引用传递和值传递
相关文章
相关标签/搜索