天天看点

我理解的call、apply、bind和__proto__、prototype

第一部分

call

apply

bind

  • call、apply、bind都是修改函数内部的this指向。
  • 函数调用call方法,函数内部的this指向call方法的第一个参数,call方法的其他参数会作为函数的实参传递,函数会立即执行。
  • 函数调用apply方法,函数内部的this指向apply方法的第一个参数,apply方法的第二个参数是函数的实参组成的数组,函数会立即执行。
  • 函数调用bind方法,函数内部的this指向bind方法的第一个参数,然后返回一个新的函数。bind方法第一个参数之后的所有参数和新函数的所有参数依次作为旧函数的实参传递。
  • 当使用new关键字实例化fn.bind返回的函数时,fn内部this指向fn的实例。

下面是几个简单的示例:

// call.js
function fn(age) {
  console.log(this.name, age);
}
/**
* fn内部this指向全局,所以this.name返回undefined.
* 所以fn(19)会输出undefined 18。
* */
fn(18);
let cat = { name: 'kitty' };
/**
* 调用call方法后,fn内部的this被修改为指向cat对象,所以this.name返回'kitty'。
* call方法第一个参数之后的所有参数依次作为实参传给fn。
* 所以fn.call(cat, 19)会输出'kitty' 19。
* */
fn.call(cat, 19);
           
// apply.js
function fn(age) {
  console.log(this.name, age);
}
/**
* fn内部this指向全局,所以this.name返回undefined。
* 所以fn(18)会输出undefined 18。
* */
fn(18);
let cat = { name: 'kitty' };
/**
* 调用apply方法后,fn内部的this被修改为指向cat对象,所以this.name返回'kitty'。
* apply方法的第二个参数是传给fn实参组成的数组。
* 所以fn.apply(cat, [19])会输出'kitty' 19 
* */
fn.apply(cat, [19]);
           
// bind.js
function fn(age, emotion) {
  console.log(this.name, age, emotion);
}
/**
* fn内部this指向全局,所以this.name返回undefined。
* 所以fn(18, 'happy')会输出undefined 18 'happy'。
* */
fn(18, 'happy');
/**
* 调用bind方法后,fn内部的this被修改为指向cat对象,所以this.name返回'kitty'。
* bind函数会返回修改this指向后的新函数。
* bind第一个参数之后的所有参数和新函数的所有参数作为实参依次传给fn。
* 所以c('angry')会输出 'kitty' 19 'angry'。
* */
let cat = { name: 'kitty' };
let c = fn.bind(cat, 19);
c('angry');
/**
* 使用new关键字实例化fn.bind返回的函数,fn内部的this指向fn的实例。
* 所以new c('sorrow')会输出 undefined 19 'sorrow'
* */
new c('sorrow');
           

了解bind函数的特性之后,接着来写一个bind的polyfill实现:

  • 首先声明一下,这一段polyfill实现仅供学习交流使用。
  • 其次感谢别的博主,写出很多优秀的博客供我学习。
Function.prototype._bind = function(ctx) {
  let args = Array.prototype.slice.call(arguments, 1);
  let self = this;
  let fn = function() {};
  function bind() {
    let bindArgs = Array.prototype.slice.call(arguments);
    self.apply(this instanceof self ? self : ctx, [...args, ...bindArgs]);
  }
  fn.prototype = self.prototype;
  bind.prototype = new fn();
  return bind;
};
function fn(age, emotion) {
  console.log(this.name, age, emotion);
}
/**
* 调用_bind方法后,fn内部的this被修改为指向cat对象,所以this.name返回'kitty'。
* _bind函数会返回修改this指向后的新函数。
* _bind第一个参数之后的所有参数和新函数的所有参数作为实参依次传给fn。
* 所以c('happy')会输出 'kitty' 18 'happy'
* */
let cat = { name: 'kitty' };
let c = fn._bind(cat, 18);
c('happy');
/**
* 使用new关键字实例化fn._bind返回的函数,fn内部的this指向fn的实例。
* 所以new c('sorrow')会输出 undefined 18 'sorrow'
* */
new c('sorrow');
           

第二部分

__proto__

prototype

  • 每个对象都有__proto__属性。
  • 只有函数才有prototype属性。
  • 对象的__proto__属性指向其构造函数的prototype属性。
function Animal(name) {
  this.name = name;
}
Animal.prototype.sayName = function() {
  console.log(this.name);
};
let cat = new Animal('kitty');
/**
* cat 是 Animal 的实例,所以 cat 的 __proto__ 属性指向 Animal 的 prototype属性。
* 所以这里输出 true。
* */
console.log(cat.__proto__ === Animal.prototype);
           
  • 当访问对象的某个属性时,会先查找对象本身有没有这个属性,如果有就返回这个属性的值。
  • 如果没有,就通过对象的__proto__属性去其构造函数的prototype对象中查找,如果有就返回这个属性的值。
  • 如果没有,就通过构造函数prototype对象的__proto__继续向上查找,直到__proto__属性的终点null,然后返回undefined。
  • 这种通过__proto__属性将对象链接起来的这条链路就叫原型链。

接下来看一个小例子:

function Animal(name) {
  this.name = name;
}
Animal.prototype.sayName = function() {
  console.log(this.name);
}

function Cat(name, emotion) {
  this.name = name;
  this.emotion = emotion;
}
// 让Cat的prototype属性指向Animal的实例
Cat.prototype = new Animal();
Cat.prototype.sayEmotion = function() {
  console.log(this.emotion);
}
let cat = new Cat('kitty', 'happy');
/**
* cat.sayName
* 先查找cat对象内部是否具有sayName方法,而cat只有name和emotion两个属性,并没有sayName方法。
* 然后会通过cat对象的__proto__属性去构造函数Cat的prototype中查找,而构造函数Cat的prototype是Animal的实例,Cat的prototype只有name属性和sayEmotion方法,也没有sayName方法。
* 然后会通过Cat的prototype的__proto__属性去构造函数Animal的prototype中查找(因为Cat的prototype是Animal的实例),这时发现Animal的prototype中定义了sayName方法,然后返回这个方法。
* sayName方法中的this指向sayName的调用者cat,所以this.name返回‘kitty'。
* 所以cat.sayName会输出'kitty'。
* */
cat.sayName();
cat.sayEmotion();
           

继续阅读