Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

JavaScript 中 this 详解 #22

Open
liangbus opened this issue Dec 21, 2019 · 0 comments
Open

JavaScript 中 this 详解 #22

liangbus opened this issue Dec 21, 2019 · 0 comments

Comments

@liangbus
Copy link
Owner

liangbus commented Dec 21, 2019

最近又在重读《你不知道的 JS》,发现好多知识点都有点忘记了,也有一些原来没有理解透彻的,因此就写下来,加深一下理解

对于初学者来说 this,可真的是让人头大,其语义也有一定的歧义,在不同地方,可能会有不同含义,甚至是相同地方也有可能在不同时机下会变得不一样,实在是难以琢磨,当初的我也是对此倍感费解

我们在网上看得最多的对 this 的描述是,this 指向函数执行时上下文,而箭头函数则指向声明时的上下文,那么,我们来看下例子

function foo() {
  var a = 2
  this.bar()
}
function bar() {
  console.log(this.a)
}
foo() // undefined

可见即使 bar 执行在 foo 的作用域内,但仍然无法访问到 a.
这段代码试图通过 this 来联通 foo() 和 bar() 的词法作用域,这是不可能实现的

还有一些说法会说 this 指向函数自身,那么再来看下

num = 0
function foo(num) {
  console.log(`foo was invoked ${num} times`);
  this.count++
}
foo.count = 0
foo(++num)
foo(++num)
foo(++num)
foo(++num) // foo was invoked 4 times
console.log(`foo.count = ${foo.count}`) // 0

所以函数内的 this 并非指向自身,而在函数内部使用的 this.count,实际上是在 this 所指向的作用域创建了一个值为 NaN 的变量,此处 this 指向 window

上述是对 this 的一些错误的理解,我们知道每个函数的 this 是在函数调用时被绑定的,完全取决于函数的调用位置

调用位置

调用位置是指函数被调用的位置,而非声明位置(箭头函数除外,后面再讨论箭头函数)

分析调用栈

function baz() {
  console.log('当前处于 baz 中 ', this)
  bar()
}
function bar() {
  console.log('当前处于 bar 中 ', this)
  foo()
}
function foo() {
  console.log('当前处于 foo 中 ', this)
}
baz();
// 当前处于 baz 中  Window {parent: global, postMessage: ƒ, blur: ƒ, focus: ƒ, close: ƒ, …}
console_runner-a7d19dfb8db35c27bf343618f838527f62153df43b74154074f4a8ccb026cd27.js:1 
// 当前处于 bar 中  Window {parent: global, postMessage: ƒ, blur: ƒ, focus: ƒ, close: ƒ, …}
console_runner-a7d19dfb8db35c27bf343618f838527f62153df43b74154074f4a8ccb026cd27.js:1 
// 当前处于 foo 中  Window {parent: global, postMessage: ƒ, blur: ƒ, focus: ƒ, close: ƒ, …}

可以看出,三个函数的 this 都是指向 window,也就是 baz 被调用的位置
这里也可以通过在控制台断点查看其函数内部的 scope,其 this 一直是指向 window,这里函数体内调用外部的其他函数,与 this.bar(), this.foo() 无异(非严格)。
image

绑定规则

默认绑定

从上面的例子我们可以看得知,在函数体内使用 this 访问对象,然后在全局环境中调用它,可以访问到全局环境下的同名属性,这里就是应用了 this 的默认绑定,但是如果使用了严格模式,则不能对全局对象用于默认绑定(声明时,非调用时)

var a = 10
function foo() {
  "use strict"
  console.log('a >>> ', this.a)
}
foo() // Uncaught TypeError: Cannot read property 'a' of undefined

隐式绑定

再来看一段更常见的代码

function foo() {
  console.log('this.a >>> ', this.a)
}
var o = {
  a: 101,
  fn: foo
}
o.fn() // this.a >>>  101

这里 o.fn 实际上保存是 foo 函数的引用地址,也就是说,o.fn 和 foo 是同一个东西,这里可以说函数被调用时,o 对象“拥有”或者“包含” foo 函数的引用

当 foo 被调用时(o.fn()),它的前面增加了对 o 对象的引用。当函数引用有上下文对象时,隐式绑定规则会把函数中的 this 绑定到这个上下文件对象

function foo() {
  console.log('this === o', this === o)
}
var o = {
  a: 101,
  fn: foo
}
o.fn() // this === o true

如果对象属性的引用是有链式引用,则以上一层或者说最后一层的调用位置为准:

function foo() {
  console.log('this.a >>> ', this.a)
}
var o1 = {
  a: 101,
  fn: foo
}
var o2 = {
  a: 1001,
  context: o1
}
o2.context.fn() // 101

此处引申出另一个问题,平常被隐式绑定的函数,有些场景下会丢失绑定的对象,也就是说它会应用默认绑定,如下:

var a = 2020
function foo() {
  console.log('this.a >>> ', this.a)
}
var o = {
  a: 2019,
  fn: foo
}
var doFoo = o.fn
doFoo()

还有一种更微秒的情况,是发生在函数作为参数传递,以回调函数执行时

var a = 2020 // global variable
function foo() {
  console.log('this.a >>> ', this.a)
}
function doFoo(fn) {
  fn()
}
var o = {
  a: 2019,
  fn: foo
}
doFoo(o.fn) // this.a >>> 2020

其实最重要还是记住,其传参的值,也是引用地址,真正决定 this 的,还是调用的位置

显式绑定

我们常见的 call 和 apply 就是显示绑定的例子,通过这两个方法,我们可以指定函数调用时 this 的指向,简单示例如下,call 和 apply 的区别这里不展开说明了

var a = 2020
function foo() {
  console.log('this.a >>> ', this.a)
}
var o = {
  a: 2019,
  fn: foo
}
foo.call(o)

另外一种显示绑定的方法是我们常见的 bind,由 ES5 提出 Function.prototype.bind,它会创建一个新的函数,并且指定其 this 为传入的参数上,简单的实现为

function _bind(fn, ctx){
   return function() {
     return fn.apply(ctx, arguments)
   }
}

更详细的模拟 bind 实现参见此处

new 绑定

使用 new 调用函数时,会执行如下操作:

  1. 创建(构造)一个全新的对象
  2. 这个新对象会被执行 [[prototype]] 连接(继承相关)
  3. 这个新对象会绑定到函数调用的 this.
  4. 如果函数返回值为非 object 类型,那么 new 表达式中的函数调用就会自动返回这个全新的对象

示例:

function foo(a) {
  this.a = a
}
var bar = new foo(123)
console.log('bar.a >>> ', bar) // 123

箭头函数

以上的四种绑定规则仅适用于正常的函数,ES6 中新增了一种特殊的函数声明方式:箭头函数

箭头函数不使用 this 的四条标准规则,而是根据外层(函数或者全局)作用域来决定 this

示例:

function foo(a) {
  return a => {
    // this 继承自 foo
    console.log(this.a)
  }
}
var o1 = {
  a: 2020
}
var o2 = {
  a: 2019
}
var bar = foo.call(o1)
bar.call(o2) // 2020

解析:foo() 内部创建的箭头函数会捕获调用 foo() 时的 this。由于 foo() 的 this 绑定到了 o1,所以箭头函数的 this 也绑定到了 o1,然后将其引用赋值给 bar,箭头函数的 this 无法被修改。

总结:
如果要判断一个运行中的函数 this 的绑定,就需要找到这个函数的直接调用位置,找到之后就可以按顺序应用以下规则来判断 this 绑定的对象(箭头函数除外)

  1. 由 new 调用?绑定到新建的对象
  2. 由 call 或者 apply 或者 bind 调用,绑定到指定的对象
  3. 由上下文对象调用?绑定到对应的调用上下文对象
  4. 默认:在严格模式下绑定到 undefined,否则绑定到全局对象
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant