> 学习 js 的时候, 得时刻怀有一种 “我是谁” 的思想觉悟。
## 背景
这两天在用[TS 改造过的 Koa][ts-koa-starter] 来写个小项目时, 发现如果想要把控制器方法改成非Static的话,所有的流程都正常, 但是只要访问方法, 并且使用成员属性的话就发生这样的报错:
```
TypeError: Cannot read property 'service' of undefined
```
控制器代码如下:
```js
class HomeController {
service:HomeService
constructor(){
this.service = new HomeService()
}
async hello (ctx) {
ctx.body = await this.service.hello()
}
}
```
一开始百思不得其解, 一直以为是路由注册方式的问题, 换了很多中方法去修改, 最终都没有解决。 于是采取 "统统推倒"策略。 法放弃自己一切猜测, 回过头来看报错内容本身, 这一看, 看出一脸不好意思...
报错信息说 `undefined` 找不到叫 'service' 的属性。 说明 undefined 的不是 service, 而是调用 service 的对象(在代码里面是`this`) , 也就是说,从这个报错里面,并不能得出结论说是 service 初始化失败 (**之前没有看清楚,就一直钻在这个牛角尖里面出不来**), 真正的原因是 `this` 本身根本就没有定义。
## js 中的 `this`
想到这里,于是便很容易想到 js 中 `this` 的鬼畜逻辑。 MDN 里面说到这样一段:
> 与其他语言相比,**函数的 `this` 关键字**在 JavaScript 中的表现略有不同,此外,在[严格模式](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Strict_mode)和非严格模式之间也会有一些差别。
>
> 在绝大多数情况下,函数的调用方式决定了`this` 的值。`this`不能在执行期间被赋值,并且在每次函数被调用时`this`的值也可能会不同。
如果没有使用 bind 进行绑定或者箭头解析函数, 那么 `this` 的指向一般有以下几种:
1. 全局环境下, `this` 指向的是全局对象, 在浏览器中就是 `window` , 在node中就是默认全局对象(global)
2. 在函数内使用(符合我们是上面的情况), `this` 的指向由调用模式来决定:
1. 直接调用方法的话, 非严格模式下只想全局对象, 严格模式下为 `undefined`
2. 作为对象的方法调用的话, `this` 指向**调用该函数的对象**
3. 作为构造函数的话, `this` 也是指向构造函数所在的对象
3. 如果要想把 `this` 的值从一个环境传到另一个,就要用 `call` 或者`apply` 方法。
4. ES中新增的bind方法会创建一个具有相同函数体和作用域的函数,但是在这个新函数中,`this`将永久地被绑定到了`bind`的第一个参数,无论这个函数是如何被调用的,且多次bind只会在第一次生效
5. 在[箭头函数](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Functions/Arrow_functions)中,`this`与封闭词法环境的`this`保持一致。 也就是说, 这种情况下`this`会被设置为它创建时的环境, 例如对象调用 `obj.bar()` 那么 bar 方法内的 `this`则会指向 obj 对象
更详细的信息可以看 [MDN 文档][mdn-this] 或者 [阮一峰的文章][ryf-this]
## ts 中的 `this`
从 js 的规则来看, 似乎把方法改为箭头函数或者使用 bind 方法来绑定 `this` 都能够解决问题。 由于 `hello` 方法是一个接口,调用的方式写在 koa 的内部,我们不方便直接去修改, 所以先择箭头函数试试水, 于是把 `hello` 方法改成箭头函数试试水。 结果问题解决!
但, 问题结束了吗?
我们知道, ts 是 js 的超集,所以按照上面的理解, 我们大体认为这便是 ts 中 `this` 的指向规则也是对的。 ts 代码最终还是会编译成 js 代码来运行的。 那么按照这样改的话, 为什么就可以生效了了呢?其实只要把 ts 代码通过转成对应的 js 代码就很清晰了。
复盘一下代码:(可以在 [Typescript Playground][ts-play] 中查看
```js
class User {
name:string = 'Fia'
sayhi () {t
console.log('hi ' + this.name)
}
saybye = () => {
console.log('bye ' + this.name)
}
}
let u = new User;
let hi = u.sayhi
let bye = u.saybye
u.sayhi() // hi Fia
hi() // undefined
bye() // bye Fia
```
其中 User 的定义, 生成的 js 代码如下 (ES2015)
```js
class User {
constructor() {
this.name = 'Fia';
this.saybye = () => {
console.log('bye ' + this.name);
};
}
sayhi() {
console.log('hi ' + this.name);
}
}
```
> 注: 生成 ES5 的代码要比上面这个更繁琐一些, 不过却更加清晰, 放在末尾供参考, 那真叫一个一目了然!
从上面生成的代码不难看出,普通的方法和通过箭头函数定义的方法的区别就是, 箭头函数的方法是在构造函数内被定义的, 跟类的普通属性(name) 一样, 而上面提到在箭头函数中, `this`与封闭环境的 `this` 一致。通俗一点来说, 这个函数创建时候 `this` 指向哪里, 那么函数内的 `this` 便指向那里。
而在构造函数中, `this` 指向的是对象本身。 所以在函数里面取 `this.name` 的逻辑便是我们创建对象的时候初始化的值。或者更粗暴的说, 因为“与封闭环境一致”, 而上面一行定义的 `this.name` 在方法里面自然就是生效的。
而普通方法 `sayhi()` 呢?当函数作为对象方法被调用时, 函数内部的 `this` 时有调用者决定的, 而不受函数定义的方式或位置影响。所以, 当我们使用 `o.sayhi()` 的方式来调用时, `this` 依旧指向的时对象 o ,而在把它赋值给 `hi` 后,调用者便不是 o 了, 此时, 函数相当于被当作纯函数调用, this 指向的是**全局**, 在严格模式下则是 `undefined` , 因此, 才会报前面开头那个错误。
需要再次明确的是,这不是 `let hi = u.sayhi` 这句的问题, 而是 `hi()` 的问题。 也就是前面所讲**this 指向是由调用者决定的**, 我们可以再例子代码中再写下面几行:
```js
let test = {name: 'test', hi: hi}
test.hi()
```
运行结果: hi test
因为此时方法的调用者是 `test` , 所以 `this.name = 'test'`
附:
> 例子生成 ES5 的代码
>
> ```js
> var User = (function () {
> function User() {
> var _this = this;
> this.name = 'Fia';
> this.saybye = function () {
> console.log('bye ' + _this.name);
> };
> }
> User.prototype.sayhi = function () {
> console.log('hi ' + this.name);
> };
> return User;
> }());
> ```
>
>
------
参考资料:
https://stackoverflow.com/questions/38245450/angular2-components-`this`-is-undefined-when-executing-callback-function
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Operators/this
http://www.ruanyifeng.com/blog/2010/04/using_this_keyword_in_javascript.html
[ts-koa-starter]: https://github.com/Vibing/ts-koa-starter
[mdn-this]: https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Operators/this
[ryf-this]: http://www.ruanyifeng.com/blog/2010/04/using_this_keyword_in_javascript.html
[ts-play]: https://www.typescriptlang.org/play/index.html#code/MYGwhgzhAECqEFMBO0DeAoaXoDswFsEAuCAFyQEscBzaAXmgHIAxCsRzbSATx2GghhuACwrQAFAEo0nbNmAB7HBAUgEAOhALq4xqKbQA1NFKiI6vIUmzoAX3Q3B3AEbcE9CdLoA+GXLmKyqoaWjqMru6MRiZmFgQI1nL29g5qpNAArh44CADucIhIANzoadD6DBnqTqKlCOkRHlVOEQ7NQqJS0AD03eVirGDondK9-Zk4ACYIAGZUCJPoEV1jjYMOdemkCGQeqJbETNtkjAA0-UT99sek6iNAA

再来理解ts中的this