0
点赞
收藏
分享

微信扫一扫

细读 JS | this 详解

佳简诚锄 2021-09-25 阅读 102
前端

我相信很多人会将 this 和作用域混淆在一起理解,其实它们完全是两回事。

例如 this.xxxconsole.log(xxx) 有什么不同呢?前者是查找当前 this 所指对象上的 xxx 属性,后者是在当前作用域链上查找变量 xxx

var foo = {
  bar: 'property', // 属性
  fn: function () {
    var bar = 'variable' // 变量
    console.log(this.bar)
    // 这里的 this.bar 永远不会取到 bar 变量,
    // 可能从 foo 对象或 window 对象上查找 bar 属性(视乎函数调用方式)
  }
}
foo.fn() // "property"

作用域与函数的调用方式无关,而 this 则与函数调用方式相关。

this 问题写了两篇文章:

一、this 是什么?

MDN 上原话是:

翻译过来就是:在 JavaScript 中,关键字 this 指向了当前代码运行时的对象。

如果我是刚接触 JavaScript 的话,看到这句话应该会有很多问号???

二、为什么需要设计 this?

假设我们有一个 person 对象,该对象有一个 name 属性和 sayHi() 方法。然后我们的需求是,sayHi() 方法要打印出如下信息:

var person = {
  name: 'Frankie',
  sayHi: function() {
    // 若该方法的功能是,打印出:Hi, my name is Frankie.
    // 要如何实现?
  }
}
  1. 方案一

最愚蠢的方案,如下:

var person = {
  name: 'Frankie',
  sayHi: function(name) {
    console.log('Hi, my name is ' + name + '.')
  }
}

person.sayHi(person.name) // Hi, my name is Frankie.
  1. 方案二

方案一在每次调用方法都需要传入 person.name 参数,还不如传入一个 person 参数。试图优化一下:

var person = {
  name: 'Frankie',
  sayHi: function(context) {
    console.log('Hi, my name is ' + context.name + '.')
  }
}

person.sayHi(person) // Hi, my name is Frankie.

先卖个关子,这种方案看着是不是跟 Function.prototype.call() 有点相似?

  1. 方案三

在方案二里方法的调用方式,看着还是有点不雅。能不能把形参 context 也省略掉,然后通过 person.sayHi() 直接调用方法?

当然可以,但此时的形参 context 要换成 JavaScript 中的保留关键字 this

var person = {
  name: 'Frankie',
  sayHi: function() {
    console.log('Hi, my name is ' + this.name + '.')
  }
}

person.sayHi() // Hi, my name is Frankie.

但是为什么 this.name 的值等于 person.name 的属性值呢?还有 this 哪来的?

实际上,this 可以理解为函数中的第一个形参(看不见的形参),在你调用 person.sayHi() 的时候,JavaScript 引擎会自动帮你把 person 绑定到 this 上。所以当你通过 this.name 访问属性时,其实就是 person.name

三、了解 this

到目前为止,好像 this 也没那么神秘,没那么难理解嘛!

再看:

var person = {
  name: 'Frankie',
  sayHi: function() {
    console.log('Hi, my name is ' + this.name + '.')
  }
}

var name = 'Mandy'
var sayHi = person.sayHi

// 写法一
person.sayHi() // Hi, my name is Frankie.

// 写法二,Why???
sayHi() // Hi, my name is Mandy.

上面示例中,person.sayHisayHi 明明都指向同一个函数,但为什么执行结果不一样呢?

原因就是函数内部使用了 this 关键字。上面提到 this 指的是函数运行时所在的环境。对于 person.sayHi() 来说,sayHi 运行在 person 环境,所以 this 指向了 person;对于 sayHi() 来说,sayHi 运行在全局环境,所以 this 指向了全局环境。(可在 sayHi 函数体内打印 this 来对比)

那么,函数的运行环境是怎么决定的呢?

内存的数据结构

下面先了解一下 JavaScript 内存的数据结构。JavaScript 之所以有 this 的设计,跟内存里面的数据结构有关系。

var person = { name: 'Frankie' }

上面的示例将一个对象赋值给变量 person。JavaScript 引擎会先在内存里面,生成一个对象 { name: 'Frankie' },然后把这个对象的内存地址赋值给变量 person

也就是说,变量 person 是一个地址(reference)。后面如果要读取 person.name,JavaScript 引擎先从 person 拿到内存地址,然后再从该地址读出原始的对象,返回它的 name 属性。

原始的对象以字典结构保存,每一个属性名都对应一个属性描述对象。举例来说,上面的例子 name 属性,实际上是以下面的形式保存的。

{
  name: {
    [[value]]: 'Frankie',
    [[writable]]: true,
    [[enumerable]]: true,
    [[configurable]]: true
  }
}

这样的结构是很清晰的,问题在于属性的值可能是一个函数。

var person = {
  sayHi: function() {}
}

这时,JavaScript 引擎会将函数单独保存在内存中,然后再将函数的地址赋值给 sayHi 属性的 value 属性。

{
  name: {
    [[value]]: 函数的地址,
    [[writable]]: true,
    [[enumerable]]: true,
    [[configurable]]: true
  }
}

由于函数是一个单独的值,所以它可以在不同的环境(上下文)执行。

var fn = function() {}
var obj = { fn: fn }

// 单独执行
fn()

// obj 环境执行
obj.fn()
环境变量

JavaScript 允许在函数体内部,引用当前环境的其他变量。

var sayHi = function() {
  console.log('Hi, my name is ' + name + '.')
}

上面示例中,函数体里面使用了变量 name。该变量由运行环境提供。

现在问题来了,由于函数可以在不同的运行环境执行,所以需要有一种机制,能够在函数体内部获取当前的运行环境(context)。所以, this 就出现了,它的设计目的就是在函数内部,指代函数当前的运行环境。

var sayHi = function() {
  console.log('Hi, my name is ' + this.name + '.')
}

上面的示例中,函数体内部的 this.name 就是指当前运行环境的 。

var sayHi = function() {
  console.log('Hi, my name is ' + this.name + '.')
}

var name = 'Mandy'
var person = {
  name: 'Frankie',
  sayHi: sayHi
}

// 单独执行
sayHi() // Hi, my name is Mandy.

// person 环境执行
person.sayHi() // Hi, my name is Frankie.

上面的示例中,函数 sayHi 在全局环境执行,this.name 指向全局环境的 name

person 环境执行,this.name 指向 person.name

回到本小节开头的示例中:

var person = {
  name: 'Frankie',
  sayHi: function() {
    console.log('Hi, my name is ' + this.name + '.')
  }
}

var name = 'Mandy'
var sayHi = person.sayHi

// 写法一
person.sayHi() // Hi, my name is Frankie.

// 写法二,Why???
sayHi() // Hi, my name is Mandy.

person.sayHi() 是通过 person 找到 sayHi,所以就是在 person 环境执行。一旦 var sayHi = person.sayHi,变量 sayHi 就直接指向函数本身,所以 sayHi() 就变成在全局环境执行。

在 JavaScript 中,this 的四种绑定规则,而以上的示例 this 均属于默认绑定的。

四、函数调用

上面有提到 Function.prototype.call(),下面先聊聊这个方法。

call() 允许为不同的对象分配和调用属于一个对象的函数/方法。

1. call 语法
  • thisArg(可选)
    在 function 函数运行时使用的 this 值。请注意,this 可能不是该方法看到的实际值。
    非严格模式下,若参数指定为 nullundefinedthis 会自动替换为指向全局对象
  • arg1, arg2, ...
    指定的参数列表。
2. 使用 call 方法调用函数,且不指定第一参数

此时分为两种情况:严格模式和非严格模式,两者在 this 的绑定上有区别。

// 非严格模式下
var thing = 'world'

function hello(thing) {
  console.log('hello ' + this.thing)
}

hello.call() // hello world
// 等价于以下三种方式,均打印出 hello world
hello()
hello.call(null)
hello.call(undefined)

所以下面示例会报错。

// 严格模式下
'use strict'

var thing = 'world'

function hello(thing) {
  console.log('hello ' + this.thing)
}

hello.call() // TypeError: Cannot read property 'thing' of undefined
3. 普通函数调用(小结)

在 JavaScript 中,其实所有的函数原始的调用方式是这样的:

function fn() { }

// 加糖调用
fn()
fn(x)

// 脱糖调用(desugar)& 严格模式
fn.call(undefined)
fn.call(undefined, x)

// 脱糖调用(desugar)& 非严格模式
fn.call(window)
fn.call(window, x)

fn() 其实就是 fn.call() 的语法糖形式。

普通函数调用可以总结成这样:

fn(...args)
// 等价于
fn.call(window [ES5-strict: undefined], ...args)


(function() {})()
// 等价于
(function() {}).call(window [ES5-strict: undefined])
4. 成员函数调用(方法)

这也是一种非常常见的调用方式,又回到开头的那个示例。

var person = {
  name: 'Frankie',
  sayHi: function() {
    console.log('Hi, my name is ' + this.name + '.')
  }
}

person.sayHi() // Hi, my name is Frankie.
// 等价于
person.sayHi.call(person) // Hi, my name is Frankie.

根据前面讲述的函数在内存中的数据结构,以上示例跟下面动态地将 sayHi 方法绑定到 person 上是一致的。

function sayHi() {
  console.log('Hi, my name is ' + this.name + '.')
}
var person = { name: 'Frankie' }
var name = 'Mandy'

// 动态添加到 person 上
person.sayHi = sayHi

person.sayHi() // Hi, my name is Frankie.
sayHi() // Hi, my name is Mandy.

以上两者区别是,前一个例子的 sayHi 函数只能通过变量 person 在内存中根据变量存放的对象地址找到对象,该对象下有一个 sayHi 属性,该属性的 [[value]] 存放的函数地址,所以只能通过 person.sayHi() 进行调用。而后一个例子中 sayHi 函数除了前面提到的调用方式外,还可以直接通过变量 sayHi 去调用该函数(sayHi())。

四、this 的绑定方式

this 是 JavaScript 的一个关键字,它是函数运行时,在函数体内部自动生成的一个对象,只能在函数体内部使用。

function fn() {
  this.x = 1
}

上面示例中,函数 fn 运行时,内部会自动有一个 this 可以使用。

函数的不同使用场景,this 会有不同的值。总的来说,this 就是函数运行时所在的环境对象。

1. 普通函数调用(默认绑定)

这是函数最常用的用法了,属于全局性调用,因此 this 就代表全局对象。

var x = 1

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

fn() // 1

注意,如果使用了 ES6 的 letconst 去声明变量 x 时,结果又稍微有点不同了!

let x = 1

function fn() {
  // 注意,若严格模式下,this 为 undefined,所以执行 this.x 就会报错
  console.log(this.x)
}

fn() // undefined

原因很简单。首先在 fn 运行时,this 仍然指向 window,只不过 window 对象下没有 x 属性,所以打印了 undefined

为什么会这样呢?

// 以下两种方式,其实都往 window 对象添加了 x、y 属性,并赋值。
x = 1
var y = 2

// 但在 ES6 之后,做出了改变
// 使用了 let、const 或者 class 来定义变量或类,都不会往顶层对象添加对应属性
let x = 1
const y = 2
class Fn {}
2. 作为对象方法调用(隐式绑定)

函数还作为某个对象的方法调用,这时 this 就指向这个上级对象。

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

var obj = {
  x: 1,
  fn: fn
}

obj.fn() // 1

// 这里我又再次提一下,但不解释了。如果还不懂,从头再看一遍。
fn() // undefined
3. 通过 call、apply 调用(显式绑定)

这两个方法的参数以及区别不再赘述,上面已经讲过了。

var x = 0

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

var obj = {
  x: 1,
  fn: fn
}

obj.fn.call() // 0,此时 this 指向全局对象
obj.fn.call(obj) // 1,此时 this 指向 obj 对象
4. 作为构造函数调用(new 绑定)

所谓构造函数,就是通过这个函数,可以生成一个新对象。这时,this 就指向这个新对象(实例)。

function Fn() {
  this.x = 1
}

var obj = new Fn()
console.log(obj.x) // 1

为了表明这时 this 不是指向全局对象,我们修改一下代码:

var x = 'global'

function Fn() {
  this.x = 'local'
}

var obj = new Fn()

console.log(obj.x) // "local"
console.log(x) // "global"

根据结果,我们可以看到全局变量 x 没有发生变化。

五、其他

1. 箭头函数

箭头函数,没有自己的 thisargumentssupernew.target,并且它不能用作构造函数。由于没有 this,因此它不能绑定 this

const obj = {
  x: 1,
  fn1: () => console.log(this.x, this),
  fn2: function() {
    console.log(this.x, this)
  }
}

obj.fn1() // undefined, Window{...}
obj.fn1.call(obj) // undefined, Window{...}
obj.fn2() // 1, Object{...}
2. 事件处理函数

在事件处理函数中,不同的使用方式,会导致 this 指向不同的对象,你知道吗?

六:思考题

抛下两个题,可以分析分析为什么结果不一样。

例一:

var length = 1

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

const obj = {
  length: 2,
  bar: function (cb) {
    cb()
  }
}

obj.bar(foo, 'p1', 'p2') // 1

例二:

var length = 1

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

const obj = {
  length: 2,
  bar: function (cb) {
    arguments[0]()
  }
}

obj.bar(foo, 'p1', 'p2') // 3

七、参考

举报

相关推荐

0 条评论