Featured image of post this 到底是个什么东西?

this 到底是个什么东西?

以及call、bind、apply的使用

在写 js 的时候,不可避免地会碰到 this 这个关键字,初学者往往会被 this 的指向搞得一头雾水,但实际上 this 并不是什么很难懂的东西,理解了它的逻辑,判断起来就十分轻松啦。

this 的指向

每个函数内都有一个 this 关键字,储存着函数调用者的信息。函数的 this 只有在函数调用时才能被确定,即运行时绑定 ,这意味着每次函数调用的时候,其 this 的值都有可能不同。

隐式绑定

通常情况下,this 的指向遵循“谁调用,指向谁”的原则,即 this 始终指向直接调用了这个函数的对象,这个性质也被称为隐式绑定

举个简单的例子:

1
2
3
4
5
6
7
8
const obj = {
  name: "obj",
  sayName() {
    console.log(this);
  },
};

obj.sayName(); // obj

这里直接调用 sayName 方法的是 obj 对象,那么 sayName 方法内的 this 就指向 obj

再嵌套一层的情况:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
const obj = {
  name: "obj",
  func: {
    sayHello() {
      console.log("Hello");
    },
    sayName() {
      console.log(this);
    },
  },
};

obj.func.sayName(); // func

这里直接调用 sayName 方法的是 func 对象,那么 sayName 方法内的 this 就指向 func

但有一种比较奇怪的情况:

1
2
3
4
5
6
7
8
const obj = {
  sayName() {
    console.log(this);
  },
};

const mySayName = obj.sayName;
mySayName(); // window

这里将 obj.sayName 方法赋值给了mySayName 变量,再将其直接调用,那么实际上就等同于由 window 直接调用了 sayName 方法,自然打印的是 window 了

默认绑定

我们通常情况下在最外部直接声明的函数、变量都是定义在 windows 对象上的,那么自然而然,函数直接调用时 this 指向的就是 window 对象,这也称之为 this 的默认绑定

1
2
3
4
5
6
function sayName() {
  console.log(this);
}

sayName(); // window
// 等价于 `window.sayName()`

隐式丢失

直接上例子

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
const obj = {
  name: "obj",
  fn: function () {
    console.log(this);
  },
};

function fn1(fn) {
  fn();
}
fn1(obj.fn); // window

咋一看可能有点奇怪,但是实际上这里的 obj.fn 只是作为一个函数传入了 fn1 中,在这个过程中和 obj 本身没有产生任何关系,可以理解为是

1
2
3
4
5
6
7
function fn1(fn) {
  fn();
}

fn1(function () {
  console.log(this);
}); // window

这就称之为隐式丢失

显式绑定

我们可以使用 callapplybind 方法来直接改变 this 的指向

Function.prototype.call

借用对象外部的方法:

1
2
3
Array.prototype.slice.call(arguments, 1);
// or
[].slice.call(arguments, 1);

函数的参数储存在 arguments 类数组对象中,这个对象并没法直接调用数组原型上的 slice 方法,因此可以使用 call 来“借用”数组原型上的这个方法。

1
2
3
4
5
6
7
Function.prototype.myCall = function (obj = window, ...args) {
  const symbol = Symbol();
  obj[symbol] = this;
  const res = obj[symbol](...args);
  delete obj[symbol];
  return res;
};
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
function say(greeting, word) {
  console.log(`${greeting}${this.name}${word}!`);
}

const Xiaoming = {
  name: "小明",
};

say.myCall(Xiaoming, "你好啊", "今天天气不错");
// 你好啊,小明,今天天气不错!

Function.prototype.apply

1
2
3
4
5
6
7
Function.prototype.myApply = function (obj = window, args) {
  const symbol = Symbol();
  obj[symbol] = this;
  const res = obj[symbol](...args);
  delete obj[symbol];
  return res;
};
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
function say(greeting, word) {
  console.log(`${greeting}${this.name}${word}!`);
}

const Xiaoming = {
  name: "小明",
};

say.myApply(Xiaoming, ["你好啊", "今天天气不错"]);
// 你好啊,小明,今天天气不错!

Function.prototype.bind

1
2
3
4
5
6
7
Function.prototype.myBind = function (obj = window, ...args) {
  // 需要绑定 this
  const that = this;
  return function () {
    that.apply(obj, args);
  };
};

这里的 myBind 仅仅实现了基本功能,并不支持柯里化性质,与原生 bind 函数仍有区别

1
2
3
4
5
6
7
8
9
function say(greeting, word) {
  console.log(`${greeting}${this.name}${word}!`);
}

const Xiaoming = {
  name: "小明",
};

say.myBind(Xiaoming, "你好啊", "今天天气不错")();

正如上文所说,这里调用完 say.myBind() 后,再自调用的时候,实际上 this 指向的是 window 对象。上面这里完全可以写成:

1
2
const mySay = say.myBind(Xiaoming, "你好啊", "今天天气不错");
mySay(); // this 指向 window

所以 bind 需要在返回函数前先绑定作用域,并利用闭包性质,使得返回的函数在外部调用时,也能访问到 that 变量。

特殊情况

new 关键字

在使用 new 关键字调用构造函数时,this 默认指向实例,如

1
2
3
4
5
6
7
function Person(name, age) {
  this.name = name;
  this.age = age;
  console.log(this);
}

const p1 = new Person("John", 30);

这里的 this 指向 p1 ,正因此我们可以将属性赋值到实例上

箭头函数

箭头函数内的 this 是定义该函数时所在的作用域指向的对象,而非调用时确定。箭头函数内的 this 等价于其上层作用域的 this

这里需要注意,js 中只存在全局作用域( window )、块级作用域和函数作用域,因此需要使用函数去改变对象中的作用域。

这里涉及到了从函数外部访问到函数内部作用域,因此需要使用闭包实现。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
const name = "window";

const obj = {
  name: "obj",
  child: {
    name: "child",
    func() {
      const name = "func";
      return () => {
        console.log(this.name);
      };
    },
  },
};

obj.child.func()(); // child

在这里,箭头函数中的 this 指向是其上层作用域的 this ,即函数 func 作用域的的 this ,那么自然就是其所处的 child 了。

而如果将 fun 也改写成箭头函数,则结果又有所不同:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
const name = "window";

const obj = {
  name: "obj",
  child: {
    name: "child",
    func: () => {
      const name = "func";
      return () => {
        console.log(this.name);
      };
    },
  },
};

obj.child.func()(); // window

这里打印出的是 window ,原因是最内层的箭头函数向上寻找上级作用域,找到了 func ,但是它也是一个箭头函数,所以就会继续向上寻找,那再上层的作用域就是 window 了,自然打印出的也就是 window 了。

由于对象的层级嵌套并不会造成作用域级别的改变,因此也不会改变箭头函数的 this 指向

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
const name = "window";

const obj = {
  name: "obj",
  child: {
    name: "child",
    child: {
      name: "child2",
      child: {
        name: "child3",
        func: () => {
          console.log(this.name);
        },
      },
    },
  },
};

obj.child.child.child.func();
// 仍然只会打印 window

此外,callapplybind 等函数也无法改变箭头函数的 this 指向

基于箭头函数的这个性质,我们就完全不需要在注册回调之前绑定 this 的值了,上文提到的 bind 函数也可以改写成这样

1
2
3
4
5
6
Function.prototype.myBind = function (obj = window, ...args) {
  // 不需要 const that = this
  return () => {
    this.apply(obj, args);
  };
};

这里的 this 直接就会指向调用这个方法的函数本身。使用箭头函数也可以避免在类声明中的一些 this 指向问题

Built with Hugo
主题 StackJimmy 设计