「万字总结」JavaScript的三座大山:原型(prototype)及原型链(prototype chain)

763 阅读5分钟

原型

每一个函数,包括构造函数,都会自动创建一个prototype属性,prototype属性指向当前函数的原型对象,原型对象自动创建constructor属性,constructor属性指针指向当前原型对象的构造函数。想要理解原型,首先要理解js创建的对象的方式,了解js创建对象是如何发展到原型模式的,从原型模式了解原型。

创建对象的方式

  • 使用原生构造函数创建对象
var o = new Object(); // 创建 Object 的一个新的实例
o.name = '张三'; // 添加属性或者方法
var o1 = new Object();
o1.name = '李四';
var o2 = new Object();
o2.name = '王五';
  • 使用对象字面量
var o = {
    name: '张三'
}
var o1 = {
    name: '李四'
}
var o2 = {
    name: '王五'
}
  • 工厂模式:使用原生构造函数或对象字面量创建对象,创建具有同样接口的多个对象需要重复编写很多代码,所以产生了工厂模式。
function people(name){
    var o = new Object();
    o.name = name;
    return o;
}
var newp1 = people('张三');
var newp2 = people('李四');
var newp3 = people('王五');
  • 构造函数:解决工厂模式创建的对象无法识别问题
function People(name){
    this.name = name;
    this.sayName = function(){
      console.log(this.name);
    }
}
var newp = new People('张三');
var newp1 = new People('李四');

newp.constructor == People; // true 以确保实例被标识为特定类型,相比于工厂模式,这是一个很大的好处

newp.sayName === newp1.sayName; // false 同样的function,内存中被创建了多次,每个实例都会创建,所有有了原型模式
  • 原型模式: 每个函数都会创建一个 prototype 属性,这个属性是一个对象,包含所有实例共享的属性和方法。实际上,这个对象就是通过调用构造函数创建的对象的原型。使用原型对象的好处是,在它上面定义的属性和方法可以被对象实例共享。构造函数模式中在构造函数中直接赋给对象实例的值,可以直接赋值给它们的原型。
function People(){
}
People.prototype.name = '张三';
People.prototype.sayName = function() { console.log(this.name)};
var newp = new People();
newp.sayName(); // "张三"

var newp1 = new People();

newp.sayName === newp1.sayName; // true sayName内存中只创建了一次
newp.name === newp1.name; // true '张三' 实例属性无法定制,这是原型模式的缺点
  • 构造函数 + 原型模式:构造函数模式用于定义实例属性,而原型模式用于定义共享的属性和方法。这样最大限度的节省了内存,又支持了向构造函数传参的能力,可谓是集两种模式之长。
function People(name){
    this.name = name;
}
People.prototype.sayName = function() {
    console.log(this.name);
};
var newp = new People('张三');
var newp1 = new People('李四');
newp.sayName === newp1.sayName; // true
newp.name === newp1.name; // false
  • 寄生构造函数模式(类似工厂模式,目的是防止污染原生构造函数如:Array、Object)
// 假如我们想要创建一个具有额外方法的特殊数组,不能直接修改构造函数,就可以使用寄生模式
function SpecialArray() {
  // 创建数组
  var values = new Array();
  // 添加值
  valuse.push.apply(values, arguments);
  // 添加方法,不会污染原生构造函数 Array
  values.toPipedString = function() {
    return this.join('|');
  }
  // 返回数组
  return values;
}
var colors = new SpecialArray('red', 'blue', 'green');
console.log(colors.toPipedString()); // red|blue|green

补充:构造函数 new 操作符具体干了什么

  1. 创建一个新对象
  2. 将构造函数的作用域赋给新对象(因此this指向了这个新对象)
  3. 执行构造函数中的代码(为这个新对象添加属性)
  4. 返回新对象
function _new(ctor, ...args) {
  if (typeof ctor !== 'function') {
    throw 'ctor must be a function';
  }
  // 创建新的对象
  let newObj = new Object();
  // 让新创建的对象可以访问构造函数原型(constructor.prototype)所在原型链上的属性;
  newObj.__proto__ = Object.create(ctor.prototype);
  // 将构造函数的作用域赋给新对象(this指向新对象);
  // 执行构造函数中的代码
  let res = ctor.apply(newObj, [...args]);

  let isObject = typeof res === 'object' && res !== null;
  let isFunction = typeof res === 'function';
  return isObject || isFunction ? res : newObj;
}

function people(name, age) {
  this.name = name;
  this.age = age;
  this.saySome = function(){
    console.log(this.name + '今年' + this.age);
  }
};

var newp = _new(people, '张三', 18);
newp.saySome(); // 张三今年18

原型关系

  • 每个函数(构造函数、class) 都有显示原型 prototype
  • 每个实例都有隐式原型 __proto__
  • 实例的 __proto__ 指向对应函数(构造函数、class)的 prototype 原型关系

实例基于原型的执行规则

先在实例自身属性和方法中寻找,如果找不到则自动去 __proto__ 中查找

原型链

原型对象等于另一个类型的实例,就形成原型链。

function People(){}
People.prototype.eat = function(){
  console.log('People 的 eat 方法被执行');
}

function Student(){}
Student.prototype = new People(); // Student 的原型对象是 People 的实例
Student.prototype.sayHi = function(){}

var xialuo = new Student()
xialuo.eat();

原型链

js实现继承的方法

  • 原型链继承:将父类的实例作为子类的原型
function Animal() {
  this.sayHi = function(){
    console.log('hello, world!');
  }
}
function Cat(){
  this.name = 'cat';
}
Cat.prototype = new Animal();
var newCat = new Cat();
newCat.sayHi(); // hello, world!
  • class 继承:class 用 extends 实现继承
class People { //类首字母要大写
    constructor(name) {
        this.name = name;
    }
    eat() {
        console.log(`${this.name} eat something`)
    }
}
//子类
class Student extends People {
    constructor(name,number){
        super(name);
        this.number = number;
    }
    sayHi(){
        console.log(`姓名:${this.name} 学号:${this.number}`)
    }
}

var xialuo = new Student('夏洛','001');
xialuo.sayHi(); // 姓名:夏洛 学号:001
xialuo.eat(); // 夏洛 eat something
  • 构造继承:使用父类的构造函数来增强子类实例,等于是复制父类的实例属性给子类(没用到原型)
function Animal(){
  this.sayHi = function(){
    console.log(this.name + 'hello, world!');
  }
}
Animal.prototype.sayName = function(){
  console.log(this.name)
}
function Cat(name){
  Animal.call(this);
  this.name = name || 'Tom';
}

var newCat = new Cat('夏洛');
newCat.sayHi(); // 夏洛hello, world!
newCat.sayName(); // 报错:newCat.sayName is not a function 没用到原型
  • 实例继承:为父类实例添加新特性,作为子类实例返回,可以访问父类原型中的属性和方法
function Animal(){
  this.sayHi = function(){
    console.log(this.name + 'hello, world!');
  }
}
Animal.prototype.sayName = function(){
  console.log(this.name)
}
function Cat(name){
  var instance = new Animal();
  instance.name = name || 'Tom';
  return instance;
}
var newCat = new Cat('夏洛');
newCat.sayHi(); // 夏洛hello, world!
newCat.sayName(); // 夏洛
  • 拷贝继承
function Animal(){
  this.sayHi = function(){
    console.log(this.name + 'hello, world!');
  }
}
Animal.prototype.sayName = function(){
  console.log(this.name)
}
function Cat(name){
  var animal = new Animal();
  for(var p in animal){
    Cat.prototype[p] = animal[p];
  }
  Cat.prototype.name = name || 'Tom';
}
var newCat = new Cat('夏洛');
newCat.sayHi(); // 夏洛hello, world!
newCat.sayName(); // 夏洛
  • 组合继承:通过调用父类构造,继承父类的属性并保留传参的优点,然后通过将父类实例作为子类原型,实现函数复用
function Animal(){}
Animal.prototype.sayHi = function(){
  console.log(this.name + 'hello, world!');
}
function Cat(name){
  Animal.call(this);
  this.name = name || 'Tom';
}
Cat.prototype = new Animal();
Cat.prototype.constructor = Cat;
var newCat = new Cat('夏洛');
newCat.sayHi(); // 夏洛hello, world!
var newCat1 = new Cat('马冬梅');
newCat1.sayHi(); // 马冬梅hello, world!
newCat.sayHi === newCat1.sayHi; // true
  • 寄生组合继承:通过寄生方式,砍掉父类的实例属性,这样,在调用两次父类的构造的时候,就不会初始化两次实例方法/属性,避免的组合继承的缺点
function Animal(){}
Animal.prototype.sayHi = function(){
  console.log(this.name + 'hello, world!');
}
function Cat(name){
  Animal.call(this);
  this.name = name || 'Tom';
}
(function(){
  // 创建一个没有实例方法的类
  var Super = function(){};
  Super.prototype = Animal.prototype;
  //将实例作为子类的原型
  Cat.prototype = new Super();
})();
var newCat = new Cat('夏洛');
newCat.sayHi(); // 夏洛hello, world!

ES6 class

class 是 ES6 语法规范,有 ECMA 委员会发布,ECMA 只规定语法规则,即我们代码的书写规范,不规定如何实现,以上实现方式都是v8 引擎的实现方式,也是主流的。

  • constructor
  • 属性
  • 方法

ES6 class 继承(extends,super)

//父类
class People { //类首字母要大写
    constructor(name) {
        this.name = name;
    }
    eat() {
        console.log(`${this.name} eat something`)
    }
}
//子类
class Student extends People {
    constructor(name,number){
        super(name);
        this.number = number;
    }
    sayHi(){
        console.log(`姓名:${this.name} 学号:${this.number}`)
    }
}
var xialuo = new Student('夏洛','001');
xialuo.sayHi(); // 姓名:夏洛 学号:001
xialuo.eat(); // 夏洛 eat something

class语法糖

  • constructor 方法是类的构造函数,是一个默认方法,通过 new 命令创建对象实例时,自动调用该方法。一个类必须有 constructor 方法,如果没有显式定义,一个默认的 consructor 方法会被默认添加。
function Point(x, y) {
  this.x = x;
  this.y = y;
}

Point.prototype.toString = function() {
  return '(' + this.x + ',' + this.y + ')';
}
//等同于
class Point {
  constructor(x, y) {
    this.x = x;
    this.y = y;
  }

  toString() {
    return '(' + this.x + ',' + this.y + ')';
  }
}
  • super当作函数使用相当于A.prototype.constructor.call(this, props)。当做对象使用,指向父类的原型对象。
// super当作函数使用
class A {
  constructor() {
    console.log(new.target.name); // new.target 指向当前正在执行的函数
  }
}
class B extends A {
  constructor {
    super();
  }
}
new A(); // A
new B(); // B

// super当作对象使用
//子类 B 当中的 super.c(),就是将 super 当作一个对象使用。这时,super 在普通方法之中,指向 A.prototype,所以 super.c() 就相当于 A.prototype.c()
class A {
  c() {
    return 2;
  }
}

class B extends A {
  constructor() {
    super();
    console.log(super.c()); // 2 // 当作对象
  }
}

let b = new B();

this

this的值不是在函数定义的时候决定的,而是在函数执行的时候决定的

  • 作为普通函数
function fn1(){
    console.log(this);
}
fn1(); //window
  • 使用call、apply、bind
function fn1(){
    console.log(this);
}
fn1.call({x: 100}); //{x: 100}
const fn2 = fn1.bind({x: 200});
fn2();//{x: 200}
  • 作为对象方法被调用
const zhangsan = {
    name: "张三",
    sayHi() {
        console.log(this); //this 当前对象
    }
}
  • 在class方法中调用
class People{
    constructor(name){
        this.name = name;
    }
    sayHi() {
        console.log(this);
    }
}
const zhangsan = new People('张三');
zhangsan.sayHi(); //this 当前对象
  • 箭头函数
const zhangsan = {
    name: "张三",
    wait(){
        setTimeout(()=>{
            console.log(this); //zhangsan 对象
        })
    }
}