为什么需要this

观察如下代码对比

  • var obj = {
        name: "acwink",
        age: 18,
        getName() {
           return obj.name; 
        },
        getAge() {
            return obj.age;
        }
    };
    
    var obj = {
        name: "acwink",
        age: 18,
        getName() {
           return this.name; 
        },
        getAge() {
            return this.age;
        }
    };
    
    
  • 对比两端代码,想象一下当我们对象的名称改变后,在没有this的情况下,我们需要把方法中每个用到 对象名称的地方全部修改。

  • 对于有 this 的情况下,常规调用对象方法 this === obj 所以这个时候,对象变量名的修改将不会影响原有的代码修改(解耦合)。

this在全局作用域下

  • 浏览器:this === window
  • Node: this === {}
    • Node 中的每一个js文件都是一个 module,通过加载编译后将js文件内容放到一个函数中,然后执行该函数 foo.call({}) , 所以Node 的 this 就绑定的是一个空对象。

::: tip

严格模式下,全局中的this默认绑定为 undefined

:::

this 在函数中的指向

函数的this是动态绑定的,this 根据不同的调用方式,绑定不同的对象。this 指向什么和函数的定义位置没有关系(除了箭头函数)

默认绑定

当函数作为一个独立函数调用时this就是默认绑定

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

foo(); // window

var obj = {
  foo: function() {
      console.log(this);
  }
};

var c;
(c = obj.foo)(); // window

var bar = obj.foo;
bar(); // window

上面三个函数调用的案例,都是独立函数调用,this 绑定为全局对象。

隐式绑定

当函数的调用是通过某个对象发起的调用,此时 this 就被绑定成 发起调用的这个对象

var obj = {
  foo: function() {
      console.log(this);
  }
};

obj.foo(); // {foo: f ()} === obj

显示绑定

隐式绑定有一个前提条件:

  • 必须在调用的对象内部有一个对函数的引用(比如一个属性)
  • 如果没有这样的引用,在进行调用时,会报找不到该函数的错误
  • 正是通过这个引用,间接的将this绑定到这个对象上

如果我们不希望在 对象内部 包含这个函数的引用,同时又希望在这个对象上进行强制调用,那么就必须使用如下方法

  • JS 为所有函数提供 call 和 apply 方法(这个和Prototype)有关
    • call 和 apply 第一个参数是相同的,后面的参数 apply 是一个数组, call 为参数列表
  • 这两个函数第一个参数就给this绑定的对象,如果 为空 则会默认绑定全局对象。

使用如上提供的方法,显示绑定this,称为 显示绑定

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

foo.call(window); // window
foo.call({name: "acwink"}); // {name:"acwink"}
foo.call(123); // Number对象(这里会自动将基本数据类型转换成包装类)

bind函数

上面说的 apply 和 call 函数绑定执行为一体,如果我们想再次调用,还得在通过 apply 和 call 再次绑定 this。

有什么方法能够将 this 绑定的固定呢?

  • 通过 bind 绑定,会将 传入的对象绑定到this,并且返回一个函数这个函数的this被绑定到传入的对象中,永远都不会被修改了。

  • function foo() {
        console.log(this);
    }
    
    var obj = {
    	name: "acwink"
    };
    
    var bar = foo.bind(obj);
    bar(); // obj 对象
    bar(); // obj 对象
    bar.apply(); // obj对象
    (bar.bind())(); // obj 对象 
    

::: tip

严格模式下,apply、call、bind 绑定的thisArg如果是基础数据类型,那么this会直接等于基础数据类型,非严格模式下会将 基础数据类型包装

:::

new绑定

JS 中的函数可以当作一个类的构造函数来使用,也就是使用new关键字

new关键字来调用函数的执行步骤:

  • 创建一个全新的对象
  • 将对象的原型指向prototype
  • 将这个新对象绑定到调用函数的this上(this的绑定步骤)
  • 如果没有返回其他对象,则返回这个新对象,如果有返回其他对象就返回该对象。如果返回的基本数据类型则会被忽略,并返回新创建的对象。
function Person(name, age) {
  this.name = name;
  this.age = age;

  // return {};
}

const person = new Person("acwink", 18);

console.log(person); // {name: "acwink", age: 18}
console.log(person.__proto__ === Person.prototype); // true

this 绑定规则的优先级

默认绑定规则的优先级最低

  • 其他规则存在时,就会通过其他规则来绑定 this

显示绑定优先级高于隐式绑定

  • 代码证明

  • // call、apply 和 隐式绑定比较 
    var obj = {
      name: "obj",
      foo: function () {
        console.log(this);
      },
    };
    
    obj.foo.call("abc"); // String("abc")
    obj.foo.apply("abc"); // String("abc")
    
    // bind 和隐式绑定比较
    function foo() {
      console.log(this);
    }
    var obj = {
      name: "obj",
      foo: foo.bind("abc")
    }
    
    obj.foo(); // String("abc")
    

new 绑定优先级高于隐式绑定

  • 代码证明

  • // new 绑定和隐式绑定
    var obj = {
      name: "obj",
      foo: function () {
        console.log(this);
        console.log(this === obj);
      },
    };
    
    var f = new obj.foo(); // {}  false
    

new 绑定优先级高于bind

  • 代码证明

  • // new 绑定优先级高于bind
    function foo() {
      console.log(this);
    }
    
    var bar = foo.bind("aaa");
    var obj = new bar(); // {}
    

this 规则之外 - 忽略显示绑定

当我们调用显示绑定函数,call apply bind 如果传入的 thisArg 值为 null or undefined or 不传参数。(使用默认绑定规则)此时函数将会为 thisArg 设置默认值为 window 或 {}。

如下代码所示:

  • function foo() {
        console.log(this);
    }
    
    var obj = {
        name: "acwink"
    };
    
    foo.call(obj); // obj 对象
    foo.call(null); // window
    foo.call(undefined); // window
    foo.call(); // window
    
    var bar = foo.bind(null);
    bar(); // window
    var c = foo.bind();
    c(); // window
    

this 规则之外 - 间接函数引用

观察如下代码

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

var obj1 = {
  name: "obj1",
  foo: foo
};

var bar2 = {
  name: "obj2" 
};

obj1.foo(); // obj1 对象
(obj2.foo = obj1.foo)(); // window

可以看到最后一行代码(obj2.foo = obj1.foo) 这句是一个表达式,既然是一个表达式那么必然会有一个返回结构,而这个结果就是就是 obj1.foo RHS 查找到的foo函数的地址,所以这里是对 foo 独立函数调用,使用默认绑定规则。

this 规则之外 - ES6箭头函数(arrow function)

箭头函数出现是为了解决,函数中需要依赖外层this问题,虽然可以通过作用域链来解决,但是要需要在外部额外定义一个变量。

箭头函数并不绑定this对象,此时this的引用就会从上层作用域中找到对应的this。

ES6之前需要访问外层this

var obj = {
    name: "acwink",
    foo: function() {
        const that = this;
        function bar() {
            console.log(that.name);
        }
        
        bar();
    }
};

obj.foo(); // "acwink"

ES6之后访问外层

var obj = {
    name: "acwink",
    foo: function() {
        const bar = () => {
            console.log(this.name);
        }
        
        bar();
    }
};

obj.foo(); // "acwink"

this 相关的面试题

面试题一

var name = "window";

var person = {
  name: "person",
  sayName: function () {
    console.log(this.name);
  }
};

function sayName() {
  var sss = person.sayName;
  sss(); // window: 独立函数调用
  person.sayName(); // person: 隐式调用
  (person.sayName)(); // person: 隐式调用
  (b = person.sayName)(); // window: 赋值表达式(独立函数调用)
}

sayName();


面试题二

var name = 'window'

var person1 = {
  name: 'person1',
  foo1: function () {
    console.log(this.name)
  },
  foo2: () => console.log(this.name),
  foo3: function () {
    return function () {
      console.log(this.name)
    }
  },
  foo4: function () {
    return () => {
      console.log(this.name)
    }
  }
}

var person2 = { name: 'person2' }

person1.foo1(); // person1(隐式绑定)
person1.foo1.call(person2); // person2(显示绑定优先级大于隐式绑定)

person1.foo2(); // window(不绑定作用域,上层作用域是全局)
person1.foo2.call(person2); // window

person1.foo3()(); // window(独立函数调用)
person1.foo3.call(person2)(); // window(独立函数调用)
person1.foo3().call(person2); // person2(最终调用返回函数式, 使用的是显示绑定)

person1.foo4()(); // person1(箭头函数不绑定this, 上层作用域this是person1)
person1.foo4.call(person2)(); // person2(上层作用域被显示的绑定了一个person2)
person1.foo4().call(person2); // person1(上层找到person1)

面试题三

var name = 'window'

function Person (name) {
  this.name = name
  this.foo1 = function () {
    console.log(this.name)
  },
  this.foo2 = () => console.log(this.name),
  this.foo3 = function () {
    return function () {
      console.log(this.name)
    }
  },
  this.foo4 = function () {
    return () => {
      console.log(this.name)
    }
  }
}

var person1 = new Person('person1')
var person2 = new Person('person2')

person1.foo1() // person1
person1.foo1.call(person2) // person2(显示高于隐式绑定)

person1.foo2() // person1 (上层作用域中的this是person1)
person1.foo2.call(person2) // person1 (上层作用域中的this是person1)

person1.foo3()() // window(独立函数调用)
person1.foo3.call(person2)() // window
person1.foo3().call(person2) // person2

person1.foo4()() // person1
person1.foo4.call(person2)() // person2
person1.foo4().call(person2) // person1


var obj = {
  name: "obj",
  foo: function() {

  }
}

面试题四

var name = 'window'

function Person (name) {
  this.name = name
  this.obj = {
    name: 'obj',
    foo1: function () {
      return function () {
        console.log(this.name)
      }
    },
    foo2: function () {
      return () => {
        console.log(this.name)
      }
    }
  }
}

var person1 = new Person('person1')
var person2 = new Person('person2')

person1.obj.foo1()() // window
person1.obj.foo1.call(person2)() // window
person1.obj.foo1().call(person2) // person2

person1.obj.foo2()() // obj
person1.obj.foo2.call(person2)() // person2
person1.obj.foo2().call(person2) // obj


// 

// 上层作用域的理解
// var obj = {
//   name: "obj",
//   foo: function() {
//     // 上层作用域是全局
//   }
// }

// function Student() {
//   this.foo = function() {

//   }
// }


call apply bind 实现

call 实现

// 在函数的原型对象上定义,所有Function的实例都可以通过查找原型链访问到该方法
Function.prototype.acall = function (thisArg, ...args) {
  if (typeof this !== "function") {
    throw new TypeError("thisArg not is a function!!");
  }

  // 判断thisArg 是否为 null 或 undefined, 如果是基本数据类型需要转换成对象类型
  thisArg = thisArg !== null && thisArg !== void 0 ? Object(thisArg) : window;

  const fn = this;
  const FN = Symbol("fn");

  // 使用默认绑定,此时函数的this 就绑定成了 thisArg
  thisArg[FN] = fn;
  const res = thisArg[FN](...args);
  delete thisArg[FN];

  return res;
};

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

foo.acall(); // window
foo.acall(123); // Number {123}

apply 实现

Function.prototype.acapply = function (thisArg, args = []) {
  if (typeof this !== "function") throw new TypeError("not is function");

  if (!(args instanceof Array)) throw new TypeError("not is iteartor");

  thisArg = thisArg !== null && thisArg !== void 0 ? Object(thisArg) : window;

  const fn = this;
  const FN = Symbol("fn");
  thisArg[FN] = fn;
  const res = thisArg[FN](...args);
  delete thisArg[FN];

  return res;
};

var a = 1;
var b = 2;

function sum(a, b) {
  console.log(this.a + this.b);
  console.log(a + b);
}

var obj = {
  a: 2,
  b: 3,
};

sum.acapply();
sum.acapply(obj, [10, 10]);

bind 实现

Function.prototype.acbind = function (thisArg, ...args) {
  if (typeof this !== "function") throw new TypeError();

  thisArg = thisArg !== null && thisArg !== void 0 ? Object(thisArg) : window;

  const fn = this;
  const FN = Symbol("fn");

  return function proxyFn(...argss) {
    // 如果使用的是new调用
    if (this instanceof proxyFn) {
      this[FN] = fn;
      this[FN](...args, ...argss);
      delete this[FN];
      return this;
    } else {
      // 正常调用
      thisArg[FN] = fn;
      const res = thisArg[FN](...args, ...argss);
      delete thisArg[FN];
      return res;
    }
  };
};

function Person(name, age) {
  this.name = name;
  this.age = age;
}

var obj = {
  name: "",
  age: 0,
};
const MyPerson = Person.acbind(obj, "acwink");

const person1 = new MyPerson(18);
console.log(person1, obj);
const person2 = new MyPerson(20);
console.log(person2, obj);
MyPerson(19);
console.log(obj);