原型

原型基本概念

  • prototype: 浏览器的标准属性,程序员使用的,显示原型,存在于函数中

  • __protp__: 浏览器的非标准属性,浏览器使用的,隐式原型,存在于实例对象中

  • 函数中有prototype、__protp__,实例对象中有__protp__

  • 实例的__proto__与对应函数的prototype都指向原型对象

  • 无论是构造函数还是普通函数,或者是方法,只要是函数,内部就有prototype

1.原型的产生

1.1执行-函数定义和执行函数

执行-函数定义

<!--执行函数定义-->
	function f1() {
		console.log('f1函数');
	}
	console.dir(f1);
//f1是一个引用变量,存储了函数的代码所存储的空间的地址.

执行函数

<!--执行函数,也可以说执行函数体-->
//是指函数的调用,也就是函数体中的代码执行时
    function f1() {
		console.log('f1函数');
	}
	f1()

1.2显示原型和隐式原型

  • 执行函数定义的时候,prototype显示原型就创建出来了(函数定义执行的时候产生的,在构造函数中

  • 由于函数本身也是实例对象,创建实例对象的时候产生的里面的隐式原型 __proto__就产生了,在实例对象中

深入JavaScript原型与继承_父类

[[ prototype ]] === __prototype __
//在不同版本的浏览器显示不同,左侧是一些书籍规范的书写方式

深入JavaScript原型与继承_父类_02

  1. 原型链

原型链基础

//简单的原型链
	function F1() {
	}

	F1.prototype.number = 100;

	function F2() {
	}

	//将F1的实例对象直接赋值到F2的原型上
	F2.prototype = new F1;
	console.dir(F2);
	F2.prototype.number = 200;

	function F3() {

	}
	//将F1实例化对象通过F2函数又赋值给了F3的原型
	F3.prototype = new F2();
	//这个数字等于修改了F1实例化对象的number的值
	F3.prototype.number = 300;
	let f3 = new F3();
	console.dir(F3);
	console.log(f3.number);//300
	console.log(obj.__proto__ === Object.prototype);
	console.log(Fn.prototype.__proto__ === Object.prototype);

深入JavaScript原型与继承_构造函数_03

对F2构造函数进行dir打印,最终返回的结果是,构造函数prototype指向原型对象,原型对象有一个F1的实例对象[[prototype]] 也就是__proto__ F1的实例对象指向F1的原型对象,原型对象有一个constructor指向F1构造函数。

最开始300是没找到的,在原型上找到的

进一步理解原型

//进一步理解原型
function Fn() {}
var obj = {};//实例对象
console.log();
	console.log(obj.__proto__ === Object.prototype);//?
	console.log(Fn.prototype.__proto__ === Object.prototype);//?

2.图解原型链

1.原型与原型链

深入JavaScript原型与继承_父类_04

2.原型与原型链

深入JavaScript原型与继承_原型链_05

3.原型与原型链

深入JavaScript原型与继承_父类_06

因为JS原型链设计时定义了Object 是一等公民,Function 是二等公民, 所以你再想想为什么 Function.prototype.__proto__ === Object.prototype 为什么为 true

3.原型与继承

继承(改变原型指向/借用构造函数/组合/拷贝)

1.原型链继承

原型链继承的本质是重写原型对象,代之以一个新类型的实例对象

function Animal() {
    this.value = 'animal';
}

Animal.prototype.run = function() {
    return this.value + ' is runing';
}

function Cat() {}

// 这里是关键,创建 Animal 的实例,并将该实例赋值给 Cat.prototype
// 相当于 Cat.prototype.__proto__ = Animal.prototype
Cat.prototype = new Animal(); 

var instance = new Cat();
instance.value = 'cat'; // 创建 instance 的自身属性 value
console.log(instance.run()); // cat is runing

产生的问题

  • 多个实例对引用类型的操作被篡改
function Animal(){
  this.names = ["cat", "dog"];
}
function Cat(){}

Cat.prototype = new Animal();

var instance1 = new Cat();
instance1.names.push("tiger");
console.log(instance1.names); // ["cat", "dog", "tiger"]

var instance2 = new Cat(); 
console.log(instance2.names); // ["cat", "dog", "tiger"]
  • 子类型的原型上的 constructor 属性被重写
  • 给子类型原型添加属性和方法必须在替换原型之后
  • 创建子类型实例时无法向父类型的构造函数传参

构造函数继承

function  SuperType(){
    this.color=["red","green","blue"];
}
function  SubType(){
    //继承自SuperType
    SuperType.call(this);
}
var instance1 = new SubType();
instance1.color.push("black");
alert(instance1.color);//"red,green,blue,black"

var instance2 = new SubType();
alert(instance2.color);//"red,green,blue"

缺点:

  • 只能继承父类的实例属性和方法,不能继承原型属性/方法
  • 无法实现复用,每个子类都有父类实例函数的副本,影响性能

3.组合式继承

原型链加构造函数

    function SuperType(name){
  this.name = name;
  this.colors = ["red", "blue", "green"];
}
SuperType.prototype.sayName = function(){
  alert(this.name);
};

function SubType(name, age){
  // 继承属性
  // 第二次调用SuperType()
  SuperType.call(this, name);
  this.age = age;
}

// 继承方法
// 构建原型链
// 第一次调用SuperType()
SubType.prototype = new SuperType(); 
// 重写SubType.prototype的constructor属性,指向自己的构造函数SubType
SubType.prototype.constructor = SubType; 
SubType.prototype.sayAge = function(){
    alert(this.age);
};

var instance1 = new SubType("Nicholas", 29);
instance1.colors.push("black");
alert(instance1.colors); //"red,blue,green,black"
instance1.sayName(); //"Nicholas";
instance1.sayAge(); //29

var instance2 = new SubType("Greg", 27);
alert(instance2.colors); //"red,blue,green"
instance2.sayName(); //"Greg";
instance2.sayAge(); //27

缺点:

  • 第一次调用SuperType():给SubType.prototype写入两个属性name,color。
  • 第二次调用SuperType():给instance1写入两个属性name,color。

实例对象instance1上的两个属性就屏蔽了其原型对象SubType.prototype的两个同名属性。所以,组合模式的缺点就是在使用子类创建实例对象时,其原型中会存在两份相同的属性/方法。

4.寄生式组合继承

function inheritPrototype(subType, superType){
  var prototype = Object.create(superType.prototype); // 创建对象,创建父类原型的一个副本
  prototype.constructor = subType;                    // 增强对象,弥补因重写原型而失去的默认的constructor 属性
  subType.prototype = prototype;                      // 指定对象,将新创建的对象赋值给子类的原型
}

// 父类初始化实例属性和原型属性
function SuperType(name){
  this.name = name;
  this.colors = ["red", "blue", "green"];
}
SuperType.prototype.sayName = function(){
  alert(this.name);
};

// 借用构造函数传递增强子类实例属性(支持传参和避免篡改)
function SubType(name, age){
  SuperType.call(this, name);
  this.age = age;
}

// 将父类原型指向子类
inheritPrototype(SubType, SuperType);

// 新增子类原型属性
SubType.prototype.sayAge = function(){
  alert(this.age);
}

var instance1 = new SubType("xyc", 23);
var instance2 = new SubType("lxy", 23);

instance1.colors.push("2"); // ["red", "blue", "green", "2"]
instance2.colors.push("3"); // ["red", "blue", "green", "3"]

这个例子的高效率体现在它只调用了一次SuperType 构造函数,并且因此避免了在SubType.prototype 上创建不必要的、多余的属性。于此同时,原型链还能保持不变;因此,还能够正常使用instanceofisPrototypeOf()

5. 基于class类实现继承

class Rectangle {
    // constructor
    constructor(height, width) {
        this.height = height;
        this.width = width;
    }
    
    // Getter
    get area() {
        return this.calcArea()
    }
    
    // Method
    calcArea() {
        return this.height * this.width;
    }
}

const rectangle = new Rectangle(10, 20);
console.log(rectangle.area);
// 输出 200

-----------------------------------------------------------------
// 继承
class Square extends Rectangle {

  constructor(length) {
    super(length, length);
    
    // 如果子类中存在构造函数,则需要在使用“this”之前首先调用 super()。
    this.name = 'Square';
  }

  get area() {
    return this.height * this.width;
  }
}

const square = new Square(10);
console.log(square.area);
// 输出 100

extends继承的核心代码如下,其实现和上述的寄生组合式继承方式一样

function _inherits(subType, superType) {
  
    // 创建对象,创建父类原型的一个副本
    // 增强对象,弥补因重写原型而失去的默认的constructor 属性
    // 指定对象,将新创建的对象赋值给子类的原型
    subType.prototype = Object.create(superType && superType.prototype, {
        constructor: {
            value: subType,
            enumerable: false,
            writable: true,
            configurable: true
        }
    });
    
    if (superType) {
        Object.setPrototypeOf 
            ? Object.setPrototypeOf(subType, superType) 
            : subType.__proto__ = superType;
    }
}

super继承的参数值和顺序,与打印结果

深入JavaScript原型与继承_父类_07

具体继承的顺序和父类的构造器内部顺序相关

深入JavaScript原型与继承_原型对象_08

4.原型的作用

作用一

共享数据,节省内存空间

1.实例对象一般都是通过构造函数进行创建的,

实例化对象的时候做的四件事:

  • var per = new Person('卡卡西',20)

    • 右侧的new Person叫做实例对象,也叫做匿名对象
    • 赋值之后,是把匿名对象的地址指向了per
    1. 申请一块空闲的空间,用来存储当前的实例对象
    2. 设置this为当前的实例对象(修改this的指向)
    3. 初始化实例对象中的属性和方法的值
    4. 把this作为当前对象进行返回

2.在构造函数中定义的属性及方法,仅仅是编写代码进行定义而已,而实际上里面定义的属性及方法是属于每个实例对象的,所以,创建多个对象,就会开辟多个空间,每个空间中的每个对象都有自己的属性及方法,大量创建对象,对象的方法都不是同一个方法(方法也是函数,函数代码也占用空间),为了节省内存空间,那么可以使用原型的方式,实现数据共享,节省内存空间

作用二

实现JS中的继承
1). 通过改变原型指向实现继承
2). 借用构造函数显示继承
3). 组合继承
4). 拷贝继承:浅拷贝和深拷贝(递归后再说)

5.原型相关表达式

所有的函数都是大Function的实例对象,所有函数的也是实例对象,所有已有一个__proto__指向了Function的prototype

1.instance of

instanceof 运算符用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上。

A instanceof B

B的prototype属性是否在A的实例对象的原型链上

本质:B的原型对象是否在实例对象A的原型链上

主要作用:检测构造函数B是否出现在实例对象A的原型链上

6.原型面试题

  1. 说说函数对象上的prototype属性?(prototype什么时候出现,执行函数定义的时候)

    • 执行函数定义(有可能被提升执行)创建函数对象

    • 给函数对象添加prototype属性, 属性值为空的Object实例对象, 也就是原型对象

    • 给原型对象添加constructor属性, 值为函数

    • 伪代码:

      // 给函数对象添加prototype属性, 属性值为空的Object实例对象, 也就是原型对象
      function Fn(){}
      console.dir(Fn)
      var obj ={}
      console.log(obj.__proto__===Object.prototype)
      console.log(Fn.prototype.__proto__===Object.prototype)
      // 伪代码
      this.prototype = {}  // this就是函数对象
      this.prototype.constructor = Fn
      
  2. 说说实例对象上的__proto__属性?(_proto-什么时候出现,实例化对象的时候)

    ​ JS引擎在创建实例对象时内部自动执行时, 会自动给实例对象添加__proto__属性, 值为构造函数的 prototype属性的值

    this.__proto__ = Fn.prototype  // this是实例对象
    
  3. 原型链(实际上是隐式原型链,言外之意就是和显示原型没毛关系,显示原型产生实例的一瞬间起的作用)

    • 从对象的__proto__开始, 连接的所有对象, 就是我们常说的原型链, 也可称为隐式原型链
    • 查找对象属性简单说: 先在自身上查找, 找不到就沿着原型链查找,如果还找不到返回undefined

深入JavaScript原型与继承_构造函数_09