JavaScript this 与手写call / bind / apply

123 阅读5分钟

MDN this

this 是当前执行上下文(全局/函数/ eval 执行上下文)的一个属性,在定义 this 变量时不知道它真正的值,运行时才能确定 this 值。

1. 全局执行上下文的 this

无论是否在严格模式下,在全局执行上下文中,this 都指向全局对象,在浏览器中就是 window 对象。

"use strict";

console.log(this === window); // true

this.a = 3;
console.log(window.a); // 3
console.log(a); // 3

2. 函数执行上下文的 this

在函数内部,this 的值取决于函数被调用的方式。

2.1 函数被全局调用时

函数被全局调用时:

  • 非严格模式下,this 值默认指向全局对象,浏览器中全局对象为 window
  • 严格模式下,this 为 undefined
window.color = "red";

function sayColor() {
  console.log(this); // window 对象
  console.log(this.color); // "red"
}

sayColor();
window.color = "red";

function sayColor() {
  "use strict";
  console.log(this); // undefined
  console.log(this.color); // 报错
}

sayColor();

2.2 嵌套函数的 this

this 关键字不具有变量那样的作用域机制,除了箭头函数,内部函数不会继承外部函数的 this 值。

如果内部函数被当作函数来调用,它的 this 值是全局对象(非严格模式)或 undefined(严格模式)。

function fn() {
  console.log("real", this); // {a: 100}
  const f1 = function () {
    console.log("function", this); // window
  };
  f1();
  const f2 = () => {
    console.log("=>", this); // {a: 100}
  };
  f2();
}
fn.call({ a: 100 });

2.3 函数作为对象里的方法被调用时

函数作为对象里的方法被调用时,this 被设置为调用该函数的对象。

window.color = "red";
var obj = { color: "blue" };

function sayColor() {
  "use strict";
  console.log(this); // obj 对象
  console.log(this.color); // "blue"
}

obj.sayColor = sayColor;
obj.sayColor();

2.4 当函数作为构造函数

当函数作为构造函数,使用 new 关键字调用时,this 被绑定到构造的新对象上。

function f() {
  this.a = 3;
}

var obj = new f();
console.log(obj.a); // 3

2.5 当函数被用作 DOM 事件处理函数

当函数被用作 DOM 事件处理函数时,this 指向当前监听事件的 DOM 元素 e.currentTarget。

document.getElementById("btn").addEventListener("click", click);
function click(e) {
  console.log(this === e.currentTarget); // 总是 true
  console.log(this === e.target); // 当 currentTarget 和 target 是同一个对象时为 true
  this.style.backgroundColor = "#A5D9F3";
}

3. 箭头函数的 this

箭头函数的 this 是自身定义时所在的执行上下文的 this。在函数中 this 指向外层第一个普通函数的 this,在全局代码中 this 被设置为全局对象。

箭头函数没有 prototype 原型,所以箭头函数本身没有 this,当然也就不能用 call()、apply()、bind()这些方法去改变 this 的指向。

全局调用

var foo = () => this;
console.log(foo() === window); // true

作为对象的方法调用

var obj1 = { foo: () => this };
console.log(obj1.foo() === window); // true

var obj2 = {
  bar: function () {
    return () => this;
  },
};

var fn1 = obj2.bar();
console.log(fn1() === obj2); // true

var fn2 = obj2.bar;
console.log(fn2()() == window); // true

外部函数作为构造函数调用

var m = 11;

function fn2() {
  this.m = 22;
  let b = () => {
    console.log(this.m);
  };
  b();
}

new fn2(); // 22
fn2(); // 22
var m = 11;

function fn2() {
  let m = 33;
  let b = () => {
    console.log(this.m);
  };
  b();
}

fn2(); // 11

4. setTimeout 的 this

如果 setTimeout 的回调函数是某个对象的方法,那么该方法中的 this 关键字将指向全局环境,而不是定义时所在的那个对象。

var name = 1;
var obj = {
  name: 2,
  showName: function () {
    console.log(this.name);
  },
};

setTimeout(obj.showName, 0); // 1
// 相当于
// var f=MyObj.showName
// f()

obj.showName(); // 2

如何解决这个问题? 第一种是将 obj.showName 放在匿名函数中执行。

setTimeout(function () {
  obj.showName(); // 2
}, 0);

第二种是使用 bind 方法,将 showName 绑定在 MyObj 上面。

setTimeout(obj.showName.bind(obj), 0);

5. 手写 call 和 apply

Function.prototype.call()Function.prototype.apply() 方法都是以给定的 this 值和参数调用该函数,区别仅在于接收参数的方式不同。

  • 第一个参数均是调用指定函数时的 this 值,如果函数不处于严格模式,则 null 和 undefined 会被替换为全局对象,原始值会被转换为对象。
  • 对于 call() 方法,其余参数直接作为函数的参数;
  • 对于 apply() 方法,第二个参数是数组(或类数组对象),作为函数的参数。

call() 和 apply() 的应用示例:

var a = 1;
var b = 2;

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

function add(c, d) {
  return this.a + this.b + c + d;
}

console.log(add.apply(this, [1, 2])); // 6

console.log(add.call(obj, 1, 2)); // 10

手写 call() 和 apply(),只是获取函数实参的方式不同

  • 手写 call:const realArgs = args.slice(1);
  • 手写 apply:const realArgs = args[1] || [];
Function.prototype.myCall = function (...args) {
  // 判断调用对象是否为函数
  if (typeof this !== "function") {
    throw new Error("Must call with a function");
  }

  // 判断传入上下文对象是否存在,如果不存在,则设置为 window
  const realThis = args[0] || window;

  // 处理传入的参数,截取第一个参数后的所有参数
  const realArgs = args.slice(1);
  // 手写 apply:const realArgs = args[1] || [];

  // 将函数作为上下文对象的一个属性
  const funcSymbol = Symbol("func");
  realThis[funcSymbol] = this; // this为函数本身

  // 使用上下文对象来调用这个方法,并保存返回结果
  let res = realThis[funcSymbol](...realArgs);

  // 删除新增的函数属性
  delete realThis[funcSymbol];

  return res;
};

6. 手写 bind

bind() 方法会返回一个新的函数。

  • 第一个参数是为新函数指定的 this 值;
  • 其余参数将作为新函数的参数,会插入到调用新函数时传入的参数的前面。

bind() 返回的函数也可以使用 new 运算符构造,提供的 this 值会被忽略,但前置参数仍会提供给模拟函数。

color = "red";
var o = { color: "blue" };

function sayColor() {
  console.log(this.color);
}

var bindSayColor = sayColor.bind(o);
bindSayColor(); // blue
function fn(a, b, c) {
  return a + b + c;
}

var _fn = fn.bind(null, 10);
var ans = _fn(20, 30); // 60

手写 bind()

Function.prototype.myBind = function (...args) {
  // 判断调用对象是否为函数
  if (typeof this !== "function") {
    throw new Error("Must call with a function");
  }

  // 判断传入上下文对象是否存在,如果不存在,则设置为 window
  const realThis = args[0] || window;

  // 处理传入的参数,截取第一个参数后的所有参数
  const realArgs = args.slice(1);

  // 调用 bind 的函数本身
  const _this = this;

  return function Fn() {
    // new Fn() 返回的对象 obj.__proto__ === Fn.prototype,obj 即是 this
    // 因此 this instanceof Fn 为 true 表示是 new Fn()
    return _this.apply(
      // new 调用 bind 返回的函数时,忽略 bind 提供的 this
      this instanceof Fn ? this : realThis,
      realArgs.concat(...arguments)
    );
  };
};

7. 代码题

普通函数的 this、箭头函数的 this、嵌套函数的 this

// window有一个name属性,默认为空
const obj = {
  name: "yc",
  getName1() {
    console.log(this.name);
  },
  getName2: function () {
    console.log(this.name);
  },
  getName3: () => {
    console.log(this.name);
  },
  getName4() {
    return function () {
      console.log(this.name);
    };
  },
  getName5() {
    return () => {
      console.log(this.name);
    };
  },
};

obj.getName1(); // yc
obj.getName2(); // yc
obj.getName3(); // 空
obj.getName4()(); // 空
obj.getName5()(); // yc

getName = obj.getName4();
getName(); // 空

getName = obj.getName5();
getName(); // yc

obj.getName3.bind({ name: "yc" });
obj.getName3(); // 空
function fn() {
  console.log("real", this); // {a: 100}
  const f1 = function () {
    console.log("function", this); // window
  };
  f1();
  const f2 = () => {
    console.log("=>", this); // {a: 100}
  };
  f2();
}
fn.call({ a: 100 });
let num = 11;
function fn() {
  this.num = 22;
  console.log(num); // 11
  let fn2 = () => {
    console.log(this.num);
  }; // 22
  fn2();
}
new fn();