this 因何存在

看下面的代码:

  1. function person() {
  2. return this.name;
  3. }
  4. function speak() {
  5. return `hello ${person.call(this)}`;
  6. }
  7. let me = {
  8. name: "lilei"
  9. }
  10. let you = {
  11. name: "hanmeimei"
  12. }
  13. person.call(me);
  14. person.call(you);
  15. speak.call(me);
  16. speak.call(you);

输出如下:
image.png
通过上面可以发现无需传递形参就可以返回不同的结果。为什么?因为this提供了隐式传递参数的机制,传递的内容是某个对象的地址。如果不使用this就需要显示传递形参,当开发大型项目时参数过多会导致混乱不堪,而把参数挂到this对象上就避免了这种情况,代码更加简洁,但可读性会降低,初接触可能产生困惑。从更抽象角度看,this成为了连接对象和函数的桥梁。

this 如何绑定

默认绑定

  1. 默认绑定的意思是当不满足其他绑定规则时,执行默认绑定规则。默认绑定会把this绑定到全局对象(浏览器里面是window对象),代码如下:
  1. let num = 0;
  2. function foo() {
  3. let num = 1;
  4. return this.num;
  5. }
  6. foo(); // undefined

输出如下:
image.png
上面的代码实现了默认绑定,从Scope->Local里我们看到,最终this指向window对象,由于使用let定义num变量,故在window上访问num值为undefined。而值为0的num存在于Scope->Script中。

隐式绑定(上下文绑定)

  1. 函数的地址被某个对象的属性引用,并通过对象的属性运行该函数(对象提供了上下文),就会触发this的隐式绑定,this会被绑定成当前对象。
  1. let obj = {
  2. name: "shadow",
  3. foo: foo
  4. }
  5. function foo() {
  6. debugger;
  7. return this.name;
  8. }
  9. obj.foo(); // shadow

输出如下:
image.png
在此补充一点,不管对象嵌套多深,this只会绑定为直接引用该函数地址的对象。

隐式绑定的丢失

代码如下:

  1. let obj = {
  2. name: "shadow",
  3. foo: foo
  4. }
  5. function foo() {
  6. return this.name;
  7. }
  8. function doFoo(fn) {
  9. fn();
  10. }
  11. doFoo(obj.foo); // undefined

输出如下:
image.png
当执行doFoo函数时,函数形参fn接收实参obj.foo,这里有一个赋值操作,相当于执行了fn=obj.foo,obj.foo指向foo函数的地址,fn函数由此获得了foo函数的地址,这个很重要,因为fn函数执行离开了obj对象的属性直接运行。输出如下:
image.png
在函数调用栈执行到foo函数时,由于丢失了和上下文obj对象的关系,因此foo函数的Scope->Local->this指向window,即执行了默认绑定。通过赋值导致隐式绑定关系丢失很常见,需要格外注意。

显示绑定

  1. Function对象的prototype上提供了3个方法来显示绑定this,分别是callapplybind方法,三个方法的简单实现如下:
  1. Function.prototype.call2 = function(ctx, ...args) { // ...为rest运算符,接收若干形参
  2. ctx = ctx || window;
  3. ctx.fn = this; // ctx.fn指向当前函数
  4. let rs = ctx.fn(...args); // 以隐式绑定方式调用当前函数
  5. delete ctx.fn; // 避免对实参ctx产生影响
  6. return rs;
  7. }
  8. Function.prototype.apply2 = function(ctx, args) { // args为数组参数
  9. ctx = ctx || window;
  10. ctx.fn = this; // ctx.fn指向当前函数
  11. let rs = ctx.fn(...args); // 以隐式绑定方式调用当前函数
  12. delete ctx.fn; // 避免对实参ctx产生影响
  13. return rs;
  14. }
  15. Function.prototype.bind2 = function(ctx, ...args) { // ...为rest运算符,接收若干形参
  16. ctx = ctx || window;
  17. return () => {
  18. return this.apply(ctx, args);
  19. }
  20. }

new 绑定

  1. js中的构造函数和传统oop语言(例如:java)中的构造函数执行原理不一样。java中执行new ClassName时调用ClassName中的构造函数(构造函数又可能调用父类的构造函数,通过super调用)创建一个新的对象。js中的构造函数被new操作符调用时,它不属于某个ClassName,而只是一个函数。

使用new操作符调用构造函数会执行四步:

  1. 创建一个新对象;
  2. 新对象的隐式原型(proto)指向构造函数显式原型(prototype)的地址;
  3. 构造函数的this指向新对象;
  4. 构造函数如果有返回值并且返回值为对象,则返回对象(可实现单例模式),否则返回新对象;

代码如下:

  1. function Foo(name) {
  2. this.name = name;
  3. }
  4. Foo.prototype.getName = function() {
  5. return this.name;
  6. }
  7. let foo = new Foo("shadow");
  8. foo.getName(); // shadow
  9. foo.__proto__; // {getName: ƒ, constructor: ƒ, ...}
  10. Foo.prototype; // {getName: ƒ, constructor: ƒ, ...}
  11. foo.__proto__ === Foo.prototype; // true

输出如下:
image.png
使用构造函数实现单例模式,代码如下:

  1. let instance;
  2. function Singleton(name) {
  3. if(instance) return instance;
  4. instance = this;
  5. instance.name = name;
  6. return instance;
  7. }
  8. Singleton.prototype.getName = function() {
  9. return this.name;
  10. }
  11. let singleton = new Singleton("shadow");
  12. let singleton2 = new Singleton("shadow2");
  13. singleton.getName(); // shadow
  14. singleton2.getName(); // shadow
  15. singleton === singleton2; // true

输出如下:
image.png

this优先级

new 绑定 > 显示绑定 > 隐式绑定 > 默认绑定。

es6 中的 this

  1. es6中的this可以通过箭头函数直接绑定在该函数执行作用域上(不是上下文)。代码如下:
  1. function foo() {
  2. return () => {
  3. return this.name;
  4. }
  5. }
  6. let obj = {
  7. name: "shadow"
  8. }
  9. let faith = {
  10. name: "faith"
  11. }
  12. let bar = foo.call(obj);
  13. bar.call(faith); // shadow

输出如下:
image.png
foo函数的执行作用域为obj对象(call函数显示绑定),因此foo内箭头函数的Scope->Local->this指向obj对象。