JS开发者应懂的33个概念系列15--this,bind,call,apply

403 阅读3分钟

话说,只有掌握了JS中this用法才算真正的跨过了JS的门槛

this是什么?

指向对象的this

想必大家都记得那句话“this 永远指向最后调用它的那个对象”。这也就是隐式绑定的规则,函数被调用时先看一看点号左侧。

var cat = {
    name: "kitty",
    getName: function() {
        console.log(this.name);
    }
}
cat.getName(); // "kitty"

为了避免不必要的麻烦,这也是最常用的一种方式。但是树林大了,什么鸟都有,我们也要去了解更多的用法,才能以不变应万变。

指向window的this

什么样子的this是会指向window呢?

我们对上面的例子进行一个小小的改动

var cat = {
    name: "kitty",
    getName: function() {
        console.log(this.name);
    }
}
var realName = cat.getName;
realName(); // undefined

当然,我们按照前面的口诀,查找调用这个函数的对象的时候,即这个函数被调用时点号的左侧,并未发现没有任何对象,对于此口诀已经不实用,我们已然找不到this是谁了?那这个时候this指向那里呢?那就是window,而在全局中并没有定义 name属性,所以我们会得到: undefined。 如果我们规定使用了ES5中的"use strict"的严格模式,那么我们就会收到报错:

Uncaught TypeError: Cannot read property 'name' of undefined

使用ES6箭头函数时的this

为了避免ES5中的this的问题,ES6引入了箭头函数,箭头函数的this指向的是定义时的对象,而非运行时。看个例子:

var name = "miao";
var cat = {
    name: "kitty",
    getName: function() {
        console.log(this.name);
    },
    getArrowName: () => {
        console.log(this.name);
    }
}
cat.getName(); // "kitty"
cat.getArrowName(); // "miao"

ES6中的this规则:箭头函数中没有 this 绑定,必须通过查找作用域链来决定其值,如果箭头函数被非箭头函数包含,则 this 绑定的是最近一层非箭头函数的 this,否则,this 为 undefined

使用new时的this

function getName(name) {
    this.name = name;
}
var newName = new getName("kitty");
console.log(newName.name); // "kitty"

new的实现原理

new getName("kitty")
其实就做了这样的工作:
(function(){
    var obj = {};
    obj.__proto__ = getName.prototype;
    var result = getName.call(obj,"kitty");
    return (typeof result === 'object' && result) ? result : obj;
})()

使用call时的this

上面提到call,它是可以改变this的指向。使用和表现方式如下:

var cat = {
    name: "kitty",
    getAge: function(age, voice) {
        console.log(this.name + " is " + age + " years old. " + voice);
    },
}
cat.getAge(5, "miao"); // "kitty is 5 years old. miao"
var dog = {
    name: "blank"
}
cat.getAge.call(dog, 2, "wangwang") // "blank is 2 years old. wangwang"

call的实现原理

划重点:面试题常考类型,小本本记一下:

Function.prototype.newCall = function(content) {
  // 当call传入的对象是null的时候,或者其他一些类型的时候,函数会报错。
  if(typeof content === 'object') {
    content = content || window;
  } else {
    content = Object.create(null);
  } 
  content.fn = this;
  var keys = Array.from(arguments);
  var arg = keys.slice(1);
  content.fn(...arg);
  delete content.fn;
}
var cat = {
  name: "Kitty",
  getAge: function(age, voice) {
      console.log(this.name + " is " + age + " years old. " + voice);
  },
}
var dog = {
  name: "Blank"
}
cat.getAge.newCall(dog, 2, "wangwang");

使用apply时的this

call相比较,其实就是传参的类型不同而已:

var cat = {
    name: "kitty",
    getAge: function(age, voice) {
        console.log(this.name + " is " + age + " years old. " + voice);
    },
}
var dog = {
    name: "blank"
}
cat.getAge.apply(dog, [2, "wangwang"]) // "blank is 2 years old. wangwang"

apply的实现原理

Function.prototype.newApply = function(content, params) {
  // 当call传入的对象是null的时候,或者其他一些类型的时候,函数会报错。
  if(typeof content === 'object') {
    content = content || window;
  } else {
    content = Object.create(null);
  }
  content.fn = this;
  content.fn(...params);
  delete content.fn;
}


var cat = {
  name: "Kitty",
  getAge: function(age, voice) {
      console.log(this.name + " is " + age + " years old. " + voice);
  },
}
var dog = {
  name: "Blank"
}
cat.getAge.newApply(dog, [5, "wangwangwang"]);

使用bind时的this

bind同样可以改变this的指向,只不过其返回的是个函数

var cat = {
    name: "kitty",
    getAge: function(age, voice) {
        console.log(this.name + " is " + age + " years old. " + voice);
    },
}
var dog = {
    name: "blank"
}
cat.getAge.bind(dog, 2, "wangwang")(); // "blank is 2 years old. wangwang"

bind的实现原理

// 使用了ES6的扩展运算符
Function.prototype.newBind = function(content, ...params) {
  var obj = this;
  return function(...otherParams) {
    obj.call(content, ...params, ...otherParams);
  }
}


var cat = {
  name: "Kitty",
  getAge: function(age, voice) {
      console.log(this.name + " is " + age + " years old. " + voice);
  },
}
var dog = {
  name: "Blank"
}
var newGetAge = cat.getAge.newBind(dog, 10, "wuwuwu")
newGetAge("wuwuwu");