🌱

`This` in js


var obj = {
  id: 1,
  foo: function() {
    console.log(this.id)
  },
  // foo() { } // using es6 syntax
}
obj.foo() // log 1

var fooo = obj.foo
fooo() // log undefined

var id = 2
fooo() // log 2

以上是基础的 this 绑定问题, 在 obj 环境下执行 foo(), this 绑定的是 obj, 在全局环境下, fooo() 则绑定了系统环境.

如何让 fooo 绑定 obj 呢? 可以使用 Function.prototype.bind() 强行绑定 obj:

var fooo = obj.foo.bind(obj)

再看一个现象:

var rectangle = {
  width: 10,
  height: 20,
  size: this.width*this.height
}
console.log(rectangle.size) // NaN

看起来行得通, 但是获取不到 widthheight?

简化模型如下:

var obj = {
  a: this
}
console.log(obj.a) // Window

这里的 this 指向的是 parent, 而非 obj, 在传统 js 中是没有 block scope 的, 只有 function scopeglobal scope, 证明如下:

var x = 1
let y = 1

if (true) {
  var x = 2
  let y = 2
}
console.log(x) // 2
console.log(y) // 1
function foo() { // in function scope
  var x = 3
  let y = 3
}
console.log(x) // 2
console.log(y) // 1

es6let, constblock scope 的.


var obj = {
  id: 1,
  foo: () => {
    console.log(this.id)
  },
}
obj.foo() // undefined

var id = 2
fooo() // 2

foo 改为箭头函数, 这里就会发生变化, 第一个在 obj 环境下执行结果是 undefined. 因为箭头函数是没有 this 的, this 虽然指向当前 scope, 但不包括 arrow function.

经过 babel 转译是这样的:

"use strict";

var _this = void 0;

var obj = {
  id: 1,
  foo: function foo() {
    console.log(_this.id);
  }
};
obj.foo();

var id = 2;
fooo();

在 vue 官方文档中有这么一段:

不要在选项属性或回调上使用箭头函数,比如 created: () => console.log(this.a)vm.$watch('a', newValue => this.myMethod())。因为箭头函数是和父级上下文绑定在一起的,this 不会是如你所预期的 Vue 实例

可以将箭头函数理解为 created: this, 那么这里的 this 指向的不会是 Vue 实例.

var data = { a: 1 }
var app = new Vue({ data })

app.$data === data // true

vue 实例的 data 共享外部 data, 它们指向同一个 obj.

再看一个比较明显的例子:

function foo() {
  this.x = 1
  var a = {
    x: 2,
    f: () => console.log(this.x)
  }
  a.f()
}
function bar() {
  this.x = 1
  var a = {
    x: 2,
    f() {
      console.log(this.x)
    }
  }
  a.f()
}
x = 3
function baz() {
  var a = {
    f: () => console.log(this.x)
  }
  a.f()
}
function qux() {
  "use strict"
  var a = {
    f: () => console.log(this.x)
  }
  a.f()
}
foo() // 1
bar() // 2
baz() // 3
qux() // throw undefined error

这里 foo 函数内的 a.f 因为是个箭头函数, 被转译后结果应该是这样:

function foo() {
  this.x = 1
  var _this = this;
  var a = {
    x: 2,
    f: function f() {
      console.log(_this.x)
    }
  }
  a.f()
}

bar 内的 a.f 是个传统js函数, 有自己的闭包, 在解析 this 时候会先去自己的环境查找, 即 x 为 2.

baz 在非 strict mode 下, 自己的环境找不到会去全局环境中查找, 对比 qux 可以看到现象.

所以在 component 章节中强调了 data 必须是一个函数:

data: { // bad
  count: 0
}

data: function () { // good
  return {
    count: 0
  }
}

防止多个 component 共享同一份 obj.


Until arrow functions, every new function defined its own this value […]. This proved to be annoying with an object-oriented style of programming. Arrow functions capture the this value of the enclosing context […]

class App extends React.Component {
  handleClick() {
    console.log(this) // undefined
  }
  handleClick2 = () => {
    console.log(this) // app
  }
  render() {
    return (
      <>
        <button onClick={this.handleClick}>
          clickme
        </button>
        <button onClick={this.handleClick2}>
          clickme
        </button>
      </>
    )
  }
}
ReactDOM.render(<App />, root);

这里为什么 handleClick 没有 bindhandleClick2 却有 bind 呢?

让我们简化下模型:

class Person {
  constructor(name) {
    this.name = name
  }
  foo() {
    console.log(this.name)
  }
  bar = () => {
    console.log(this.name)
  }
}
let Bob = new Person('Bob')
Bob.foo() // Bob
Bob.bar() // Bob

Person.prototype // constructor, foo

这里有点奇怪, Person 的原型上没有 bar, 如果原型上没有 bar, 那么实例 Bobbar 哪里来的呢?

通过 Babel 转移:

function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }

class Person {
  constructor(name) {
    _defineProperty(this, "bar", () => {
      console.log(this.name);
    });

    this.name = name;
  }
  foo() {
    console.log(this.name);
  }
}

可以发现实例 Bob 在初始化时候定义了 bar 这个属性, 绑定了实例的属性, 而非原型上的方法.

再回过来看上面的 react 例子, 这么理解下:

const app = new App()
const ele = app.render()
ele.btn1.onClick() // undefined
ele.btn2.onClick() // App

对应的可以写一个:

class Cpnt {
  constructor() {
    this.id = 1
  }
  foo() {
    console.log(this.id)
  }
  bar = () => console.log(this.id)
  render() {
    return {
      foo: this.foo,
      bar: this.bar,
    }
  }
}
const app = new Cpnt()
app.foo() // 1
app.bar() // 1
const ele = app.render()
ele.foo() // undefined
ele.bar() // 1

这就可以解释了在 react 中手动绑定 this 的原因, 在 ele.foo() 会去 ele 的环境下查找 id, 而 ele 并没有在 Cpnt 原型链上, 没有 id 属性.

Reference


在我们一生中,命运赐予我们每个人三个导师,三个朋友,三名敌人,三个挚爱。但这十二人总是不以真面目示人,总要等到我们爱上他们、离开他们、或与他们对抗时,才能知道他们是其中哪种角色。