原型与原型链和继承

JavaScript 中,万物皆对象,对象又分为:普通对象和函数对象,也就是Object 和 Function

基本概念

  • 对象:属性和方法的集合,即变量和函数的封装。每个对象都有一个__proto__属性,指向这个对象的构造函数的原型对象

  • 构造器函数:用于创建对象的函数,通过new关键字生成对象(构造函数的首字母必须大写,用来区分于普通函数)

  • 原型对象:每个函数都有一个prototype属性,它是一个指向原型对象的指针

  • 每个对象都有 __proto__ 属性,但只有函数对象才有 prototype 属性

普通对象与函数对象

举个栗子:

function f1(){};
var f2 = function();
var f3 = new Function('args','alert(args)');

// 函数对象 f1,f2,f3

var c1 = {};
var c2 = new Object();
var c3 = new function(){};

// 普通对象 c1,c2,c3

凡是通过new Function()创建的对象都是函数对象,其他的都是普通对象。Function Object 也都是通过 New Function()创建的所以它们也是函数对象

注意:尽管可以使用 Function 构造函数创建函数,但最好不要使用它,因为用它定义函数比用传统方式要慢得多。不过,所有函数都应看作 Function 类的实例。

构造函数

构造函数始终都应该以一个大写字母开头;构造函数本身也是函数,只不过可以用来创建对象;要创建构造函数的新实例,必须使用new操作符

举个栗子:

构造函数调用所经历的步骤:

  • 创建一个新的对象

  • 将构造函数的作用域赋给新对象(因此this指向了这个新对象)

  • 执行构造函数中的代码(为这个新对象添加属性)

  • 返回新对象

构造函数存在的问题:每个方法都要在每个实例上重新创建一遍。(每定义一个函数就是实例化了一个对象),不同实例上的同名函数是不相等的。其解决方法便是使用原型模型

原型对象

每一个函数都有prototype属性,它指向一个对象,即原型对象。

constructor指向构造函数,每个原型对象 ( Person.prototype ) 都有一个constructor属性,指向prototype属性所在的函数 ( Person )

原型链

JS 在创建对象(不论是普通对象还是函数对象)的时候,都有一个叫做 __proto__ 的内置属性,用于指向创建它的函数对象的原型对象 prototype

属性搜索原则

对象在访问属性的时候, 首先在当前对象中查找,找到就停止查找直接使用该属性,如果没有找到则顺着__proto__属性向上查找,直到查找到原型链的顶端 Object.prototype.__proto__

这种由 __proto__ 属性连接而成的链条就是 原型链

所有函数对象的 __proto__ 都指向Function.prototype,它是一个空函数(Empty function)

总结

Object 与 Function 都是函数对象,都是通过new Function()创建的,所以: Object.__proto__ 指向 Function.prototypeFunction.__proto__ 指向 Function.prototype

结论:所有函数对象的 __proto__ 都指向 Function.prototype

Function.prototype 这个函数对象比较特殊,它的 __proto__ 指向了 Object.prototype,而 Object.prototype__proto__ 指向了 null

JS继承

寄生组合继承

思想:通过寄生方式,砍掉父类的实例属性,避免了组合继承的缺点(调用两次父类构造函数)

组合继承是继承的一种实现方式,下面会有介绍

Object.create(Foo.prototype)里面的实现机制类似下面这样

// ES6 之前 Bar.ptototype = Object.create( Foo.prototype );

// ES6新增了一个方法,Object.setPrototypeOf,可以直接创建关联,而且不用手动添加constructor属性 Object.setPrototypeOf( Bar.prototype, Foo.prototype ); console.log(Bar.prototype.constructor === Bar) // true

下面介绍一些其他方式实现继承,同时也是 寄生组合继承 的演进过程

原型链继承

思想:子类的原型指向父类的实例

缺点:

  • 无法向父类的构造函数传参

  • 来自原型对象的引用属性是所有实例共享的

借用构造函数继承

思想:借用父类的构造函数来增强子类实例,等于是把父类的实例属性复制一份给子类实例装上

缺点:

  • 只能继承父类的实例属性和方法,不能继承原型属性/方法

  • 无法实现函数的复用,每个子类都有父类实例函数的副本

组合继承

思想:通过调用父类构造,继承父类的属性并保留传参的优点,然后通过将父类实例作为子类原型,实现函数复用

缺点:

  • 调用了两次父类的构造函数

原型式继承

思想:借助原型,可以基于已有的对象创建新对象

ES5新增了方法规范化了原型式继承。即: Object.create()

寄生式继承

思想:在继承对象的同时通过添加属性与方法增强这个新对象,原型式继承的加强版

ES6中的继承

ES6中可以声明类( class )提供了extends关键字实现类的继承,定义的类只是语法糖,目的是让我们用更简洁明了的语法创建对象及处理相关的继承。

super 关键字

  • 作为函数调用时( 即super(...args)), super代表父类的构造函数。

  • 作为对象调用时( 即super.method() ), super代表父类。此时super即可以引用父类实例的属性和方法, 也可以引用父类的静态方法。

  • 子类必须在constructor方法中调用super方法,并且在使用this之前调用

ES5 的继承, 实质是先创造子类的实例对象this, 然后再将父类的方法添加到this上面( base.call(this)) ES6中的继承机制完全不同,实质是先创造父类的实例对象this( 所以必须先调用super方法,返回父类实例,如果不调用super方法, 子类就得不到this对象), 然后再用子类的构造函数修改this。

类的原型( prototype )

这样的结果是因为, 类的继承是按照下面的模式实现的。

Object.setPrototypeOf方法的实现:

Extends 的继承目标

extends关键字后面可以跟多种类型的值 class Bar extends F

F 只要是一个有prototype属性的函数, 就能被 Bar 继承。 由于函数都有prototype属性( 除了Function.prototype函数), 因此F可以是任意函数。

Function.prototype.prototype // undefined

分析:

Foo就是一个普通函数,所以直接继承 Funciton.prototype 但是, Foo 调用后返回一个空对象( 即Object实例), 所以Foo.prototype.__proto__指向构造函数( Object) 的prototype属性。

Bar是一个普通函数, 所以直接继承 Funciton.prototype 但是 Bar 调用后返回的对象不继承任何方法, 所以它的 __proto__ 指向Function.prototype, 即实质上执行了下面的代码:

Object.getPrototypeOf方法可以用来从子类上获取父类。

实例的 __proto__ 属性

原生构造函数的继承

原生构造函数是指语言内置的构造函数, 通常用来生成数据结构。 ECMAScript 的原生构造函数大致有下面这些

注意, 继承Object的子类, 有一个行为差异。

上面代码中, NewObj继承了Object, 但是无法通过super方法向父类Object传参。 这是因为 ES6 改变了Object构造函数的行为, 一旦发现Object方法不是通过 new Object() 这种形式调用, ES6 规定Object构造函数会忽略参数。

Last updated

Was this helpful?