原型、原型链以及继承

570 阅读7分钟

前言

JavaScript在解决复用性方面做过很多尝试,最终确定了利用原型和原型链来解决。

而在ES6之前,JavaScript 中除了基础类型外的数据类型都是对象(引用类型),没有类(class),为了实现类似继承以便复用代码的能力,JavaScript选择了原型和原型链。

甚至在ES6之后,JavaScript也没有真正的类(class)。ES6虽然提供了class关键字让我么可以伪造一个“类”,但其实只是语法糖而已,本质上仍然是一个对象。

ES6实现的继承,本质仍是基于原型和原型链。

构造函数

使用new运算符的函数对象

function Star(name,age) {
    //实例成员
    this.name = name;
    this.age = age;
}
//静态成员
Star.sex = '女';

let stars = new Star('小红',18);
console.log(stars);      // Star {name: "小红", age: 18}

console.log(stars.name); //小红    通过构构造对象访问实例成员
console.log(Star.sex);  //女       通过构造函数可直接访问静态成员
  • 实例成员: 实例成员就是在构造函数内部,通过this添加的成员。实例成员只能通过实例化的对象来访问。
  • 静态成员: 在构造函数本身上添加的成员,只能通过构造函数来访问

可以得知new的主要作用是

  • new通过构造函数Star创建出来的实例可以访问到构造函数中的属性
  • new通过构造函数Star创建出来的实例可以访问到构造函数原型链中的属性,也就是说通过new操作符,实例与构造函数通过原型链连接了起来

new 的实现以及内部原理

  • 创建一个空对象,并且 this 变量引用该对象,同时还继承了该函数的原型
  • 属性和方法被加入到 this 引用的对象中
  • 新创建的对象由 this 所引用,并且最后隐式的返回 this
function _new(Fn) {
    var obj = {},  //创建一个空的对象 和new Object()一个道理
    Fn= [].shift.call(arguments);//剪切出参数一 也就是被new的函数
    // 链接到原型,obj 可以访问到构造函数原型中的属性
    obj.__proto__ = Fn.prototype;//绑定原型链 ❗继承原型方法
    Fn.apply(obj, arguments);//将new构造函数的this❗静态方法 给到obj对象上
    return obj;
};

ES6版本

function _new(Fn, ...arg) {
    let obj = Object.create(Fn.prototype)//原生优化  等同于 -->  var obj = {},   obj.__proto__ = Fn.prototype
    //Fn当前需要被new实例的类   ...arg 传参列表
    Fn.call(obj, ...arg);//将new的this指向Fn的原型
    return obj
}
//优化
function myNew(fn, ...args) {
    let instance = Object.create(fn.prototype);
    let result = fn.call(instance, ...args)
    return typeof result === 'object' ? result : instance;
}

原型

原型对象

  • 在声明了一个函数之后,浏览器会自动按照一定的规则创建一个对象,这个对象就叫做原型对象(prototype)
    • 声明函数后,这个构造函数(声明了的函数)中会有一个属性prototype,这个属性指向的就是这个构造函数对应的原型对象;
  • 原型对象中有一个属性constructor,这个属性指向的是这个构造函数(new 函数对象) 原型上可以自定义方法也可以用原型上自带的方法;共享给继承改原型链子级自己的属性和方法
function A() {}
let a = new A()
A.prototype.x = 2
console.log(a.x)//1

__ proto__ 以及原型链

实例对象独有的,指向生成该对象的函数的原型

  • 主要作用是可以对对象属性的访问修改和删除,以及解决继承问题

原型链

  • 原型与原型层层相链接的过程,当对象需要某个属性时,当前对象没有该属性时,就会查找它的原型是否有,一直递归原型对象
  • __proto__是任何对象都有的,而且js万物皆对象,他们会连起来最终指向 null 空对象

ES6 Class

class 作为构造函数的语法糖,同时有prototype属性和__proto__属性,因此同时存在两条继承链。

  • 子类的__proto__属性,表示构造函数的继承,总是指向父类。
  • 子类prototype属性的__proto__属性,表示方法的继承,总是指向父类的prototype属性。
class A {
}

class B extends A {
}

B.__proto__ === A //>> true
B.prototype.__proto__ === A.prototype //>> true

ES5和ES6的继承区别

  • ES6 class 内部所有定义的方法都是不可枚举的;

  • ES6 class 默认即是严格模式;

  • ES6的继承机制(extends )实质上是先创建父类的实例对象this(所以必须先调用父类的super()方法),然后再用子类的构造函数修改this。

  • ES5的继承是通过prototype或构造函数机制来实现。

  • ES6 class 子类必须在构造函数中调用super(),这样才有this对象;

  • ES5中类继承的关系是相反的,先有子类的this,然后用父类的方法应用在this上。

继承

原型继承

function Parent2(name) {
    this.name = name;
    this.play = [1, 2, 3]
}
Parent2.prototype.played = function () {
    return this.play;
}
function Child2() {
    this.type = 'child2';
}
Child2.prototype = new Parent2('parent');
//子级无法传参给父级  如果没传parent会显示undefined
let obj = new Child2('childName')
console.log(obj.name)
//子级能够修改到父级属性和方法
var s1 = new Child2();
var s2 = new Child2();
s1.play.push(4);
console.log(s1.played(), s2.played());
  • 优点:
    • 继承了父类的属性和方法,又继承了父类的原型对象
  • 缺点
    • 无法实现多继承(因为已经指定了原型对象了)
    • 子类可以重写父类方法(这样会导致继承父类的其他实例也受到影响)
    • 创建子类时,无法向父类构造函数传参数

call继承(构造继承)

function Parent(name, sex) {
    this.name = name
    this.sex = sex
    this.colors = ['white', 'black']
}
Parent.prototype.a = function () { console.log(123) }
function Child(name, sex) {
    Parent.call(this, name, sex)
}
//子级修改父级不会影响到
var child1 = new Child('child1', 'boy')
child1.colors.push('yellow')
console.log(child1)//Child { name: 'child1', sex: 'boy', colors: [ 'white', 'black', 'yellow' ] }
var child2 = new Child('child2', 'girl')
console.log(child2)//Child { name: 'child2', sex: 'girl', colors: [ 'white', 'black' ] }

//子级无法继承父级的原型属性和方法
child1.a()//child1.a is not a function

//子级
console.log(child1 instanceof Child)//是属于chiild实例的
console.log(child1 instanceof Parent)//但不是parent的实例
  • 优点:
    • 解决了原型链继承中子类实例共享父类引用对象的问题,实现多继承,创建子类实例时,可以向父类传递参数
  • 缺点:
    • 只能继承父类实例的属性和方法,不能继承原型的;
    • 实例并不是父类的实例,只是子类的实例
    • 无法实现函数复用,每个子类都有父类实例函数的副本,影响性能
    • 由于它是复制了父类构造函数中的属性和方法,这样每个子级都复制了各自属性和方法, 可是有的方法完全没有必要复制,可以用来共用的所以就说不能够「函数复用

组合继承 (前2个的组合)

function Father(name) {
    this.name = name;
    this.colors = ["red", "blue", "green"];
}
Father.prototype.sayName = function () {
    console.log(this.name);
};
function Son(name, age) {
    Father.call(this, name);//继承实例属性,第一次调用Father()
    this.age = age;
}
Son.prototype = new Father();//继承父类方法,第二次调用Father()
Son.prototype.constructor = Son; //让原型的构造器指向他

let obj = new Son('汤姆',18)
console.log(obj)

obj.sayName() // 使用父类的方法
  • 实现方式:
    • 使用原型链继承来保证子类能继承到父类原型中的属性和方法
    • 使用构造继承来保证子类能继承到父类的实例属性和方法
  • 优点:
    • 可以继承父类实例属性和方法,也能够继承父类原型属性和方法 弥补了原型链继承中引用属性共享的问题
    • 可传参,可复用
  • 缺点:
    • 使用组合继承时,父类构造函数会被调用两次,并且生成了两个实例, 子类实例中的属性和方法会覆盖子类原型(父类实例)上的属性和方法,所以增加了不必要的内存。

原型式继承

ES6的Object.create()代替了它
function create (obj) {
    var newObj = {}
    newObj.__proto__ = obj
    return newObj;
}
  • 优点:
    • 再不用创建构造函数的情况下,实现了原型链继承,代码量减少一部分。
  • 缺点:
    • 一些引用数据操作的时候会出问题,两个实例会公用继承实例的引用数据类 谨慎定义方法,以免定义方法也继承对象原型的方法重名 无法传参

寄生继承(了解)

function createAnother(original){
 var clone = object(original);//通过调用object函数创建一个新对象
 clone.sayHi = function(){//以某种方式来增强这个对象
  alert("hi");
 };
 return clone;//返回这个对象
}
  • 缺点(同原型式继承)

寄生组合方法(了解)

function extend(subClass,superClass){
 var prototype = object(superClass.prototype);//创建对象
 prototype.constructor = subClass;//增强对象
 subClass.prototype = prototype;//指定对象
}

over 仅个人笔记收录

本文使用 mdnice 排版