笔记 new、call、apply、bind、继承

196 阅读3分钟

new

new被调用后做了三件事情:

  1. 让实例可以访问到私有属性
  2. 让实例可以访问构造函数原型(constructor.prototype)所在原型链上的属性
  3. 如果构造函数返回的结果不是引用数据类型,返回实例(一般构造函数都没有return,所以都是返回实例)
  /**
   *
   *   使用new调用构造函数会经历如下过程:
   *   1. 创建一个对象
   *   2. 将构造函数的作用域赋给新对象
   *   3. 执行构造函数中的代码
   *   4. 返回新对象
   *
   * @returns
   */
  function myNew() {
      let obj = new Object(); // 寄生对象
      let Constructor = Array.prototype.shift.call(arguments);    // 第一项参数,即构造函数
      obj.__proto__ = Constructor.prototype;  // 核心代码,确定实例与构造函数的关系,这样obj就可以访问到构造函数原型中的属性
      let res = Constructor.apply(obj, arguments);    // 执行构造函数中的内容,返回res。注意在这个地方将this绑定到了obj上
      return typeof res === 'object' ? res : obj; // 如果返回结果不是对象,就返回一个空对象
  }


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

  let person = myNew(Person, '张三', 15);

call和apply

/**
 * 实现整体步骤:
 *  1. 将函数设为对象的属性
 *  2. 执行该函数
 *  3. 删除该函数
 * 
 * 另外:
 *  1. apply可以接收数组(或类数组对象)作为第二个参数,...会将数组展开传给要执行的函数
 *  2. 当this为null时,默认为window
 *  3. 返回拥有指定this和参数的函数的执行结果
 */
Function.prototype.call = function (context, ...args) {
    var context = context || window; // 传入绑定的上下文对象
    context.fn = this; // this 指向函数绑定的对象,函数对象也是对象,这里 this 就是调用时的函数
    var result = eval('context.fn(...args)'); //eval() 函数会将传入的字符串当做 JS 代码进行执行,result 接收结果并返回
    delete context.fn // 为了不影响绑定的对象
    return result;
}

bind

bind 和 call/apply 的区别在于:

  1. call/apply 是调用函数,函数立即执行,返回结果;bind 不调用函数,返回函数
  2. 参数不同,bind 只有 context 一个对象参数,call 和 apply 除了需要对象,还要传入函数的参数(因为要立即执行),apply 传数组

bind 注意点:

  1. 对于普通函数,绑定this指向
  2. 对于构造函数,要保证原函数的原型对象上的属性不能丢失
  3. bind()创建的函数没有prototype ( 箭头函数也没有)
/**
 * 特点:
 *  1. 返回一个函数
 *  2. 可以传入参数
 *  3. 执行返回的函数时依然能传入参数
 *  4. bind 返回的函数作为构造函数的时候,bind 时指定的 this 值会失效,但传入的参数依然生效
 */
Function.prototype.bind = function (context, ...args) {
    if (typeof this !== "function") {
      throw new Error("Function.prototype.bind - what is trying to be bound is not callable");
    }

    var self = this;

    var fbound = function () {
    	// 绑定 this 指向
        // 当作为构造函数时,this 指向实例,self 指向绑定函数,因为下面一句 `fbound.prototype = this.prototype;`,已经修改了 fbound.prototype 为 绑定函数的 prototype,此时结果为 true,当结果为 true 的时候,this 指向实例。
        // 当作为普通函数时,this 指向 window,self 指向绑定函数,此时结果为 false,当结果为 false 的时候,this 指向绑定的 context。
        self.apply(this instanceof self ? this : context, args.concat(Array.prototype.slice.call(arguments)));
        // Array.prototype.slice.call() 将具有length属性的对象转成数组, ES6 可以用 ... 或 Array.from()
    }

    fbound = Object.create(this.prototype); // create()方法创建一个新对象,使用现有的对象来提供新创建的对象的__proto__,保证原函数的原型对象上的属性不丢失

    return fbound;
}

原型链继承

/**
 * es5继承 Student继承Person 组合继承
 * 父类构造函数会执行两次,如果构造函数比较复杂就造成浪费
 */
function Person(name, gender) {
    this.name = name;
    this.gender = gender;
}
Person.prototype.sayName = function () {
    console.log(this.name)
}
function Student(name, gender, grade) {
    // 执行一次父类
    Person.call(this, name, gender);
    this.grade = grade;
}
Student.prototype = new Person(); // 又执行一次
Student.prototype.constructor = Student;
const student = new Student('张三', '男', '大三');
student.sayName();
console.log(student);

ES6 继承

/**
 * es6继承 Student继承Person
 */
class Person {
    constructor(name, gender) {
        this.name = name;
        this.gender = gender;
    }
    sayName() {
        console.log(this.name);
    }
}
class Student extends Person {
    constructor(name, gender, grade) {
        super(name, gender);
        super.test = 1; // 当不调用super时,super为this,最终test在Student上
        this.grade = grade;
    }
}
const student = new Student('张三', '男', '大三');
student.sayName();
console.log(student);