JS原型的理解
关于 JS 的原型和原型链,其实这个概念在我的心中,有个大概的形状,但是总是不能够很清楚的描述出来,当别人问我到底什么是原型,我在想我该怎么回答,所以就整理了这篇文章。
说原型之前,我们需要明确一个概念,在 JS 中有一个概念:万物皆对象。任何复杂数据类型,都可以最终抽象为Object,包括函数,他是一个特殊的对象。
方向
- 什么是
prototype - 如何使用
prototype
什么是 prototype
我们先来看一个例子
console.log(Object.prototype);console.log(Array.prototype);console.log(Boolean.prototype);console.log(Function.prototype);console.log(String.prototype);console.log(Date.prototype);console.log(Number.prototype);console.log(Error.prototype);console.log(RegExp.prototype);console.log(Promise.prototype);console.log(Symbol.prototype);console.log(Map.prototype);console.log(Set.prototype);// ...

在这里我们就能大概的感觉 prototype 的模糊概念。其实 prototype 是 JS 实现面向对象的一个重要机制。上面的都是函数,在 JS 中本质上也是对象(Function),函数对象都有一个子对象:prototype 对象。在 JS 中类是以函数进行定义的。prototype 表示该函数的原型,也表示一个类的成员的集合。
一句话概括:在 JavaScript 中,每一个 javascript 对象(除 null 外)创建的时候,就会与之关联另一个对象,即每个函数都有一个 prototype 属性,这个属性指向函数的原型对象。
例如:
function Person(name) {this.name = name;}Person.prototype.sayName = function() {console.log(this.name);};const Tom = new Person("tom");const Jack = new Person("jack");Tom.sayName(); // tomJack.sayName(); // jack
上述例子,Person 是一个构造函数,Tom,Jack 是具体的实例函数对象。我们使用 new 创建出来的都是实例对象。而实例对象就是根据原型 prototype 这个模板,被生产出的属于自己特色的东西。
我们在阐述比较复杂的概念,还是先解释一下构造函数和实例原型之间的关系

在这里,我们可以看见两个关键词,构造函数与实例原型,其实构造函数都比较容易理解,实例原型其实按照字面理解就是由 Person 创建的实例的原型,他是由 Person 创建的实例对象的原型。
但是这是从构造函数上的属性(即:Person.prototype),那么在真正创建的实例对象上面,也能使用 Tom.prototype 进行访问么?
通过实践,我们能够清楚的知道使用上述
Tom.prototype是不行的,但是通过Tom.__proto__却可以访问到。
那么,我们就要引出另外一个关键词,历史的遗留产物:__proto__
那么 prototype 和__proto__之间有什么区别和联系呢?我们可以再打印两个东西
console.log(Person.prototype);console.log(Person.__proto__);console.log(Tom.prototype);console.log(Tom.__proto__);

那么,再结合:

结合上图,再结合下列的打印:
console.log(Tom.__proto__ === Person.prototype); // true
到这里,我们可以进行一个总结:
在实例对象中使用__proto__,它相当于一个对外暴露的访问器,相当于setter和getter可以方便的访问到当前实例对象的实例原型。我们从浏览器打印 Person.prototype 的打印结果:
sayName: ƒ ()constructor: ƒ Person(name)__proto__: Object
我们可以看到 Person.prototype 中也有一个__proto__,其实这里我们可以这样理解。
在 JS 中允许这样一个事实:
- 所有的对象都含有
__proto__和constructor。 prototype属性是函数所独有的。
但在 JS 中函数也是一种对象,所以也拥有__proto__和 constructor 属性。
结合上述的阐述,解释构造函数 Person
Person 相对于 Tom 来说,Person 是构造函数,Tom 是实例对象。
Person 相对于 Function 来说,Function 是构造函数,Person 是实例对象,但是这个实例对象是一个函数。
Function 相对于 Object 来说,Object 是构造函数,Function 是实例对象,但是这个实例对象是一个函数。
这种说法的准确性还有待商榷,但是表达的还是一个意思:如果实例对象是一个函数实例对象,可以用来生产再创造,那么他就有原型,如果产生的实例对象无法进行再创造,那么相对的来说实例对象就是私有的,独立的,个体性质的,所以没有原型,所以找原型也是找他的父函数的(创造他的构造函数)原型,通过构造函数创建的子实例对象继承了父函数的一些性质,所以子实例对象也是继承了父函数的原型,通过proto来访问

这张图简单的解释就是:
它只解释了构造函数 Person 与实例对象 person1 和 person2(可以理解为 Tom 和 Jack)之间的关系。
当我们在构造函数中,去访问构造函数的原型,实际上就是类似于这种地址的引用,将保存 prototype 的地址就引用自 Person Prototype 这个地址块。也就可以理解成 prototype 就是类似于一个指针,由Person Prototype -> Person。
在创建的实例对象中,也提供了一个简便访问原型的属性就是,__proto__,他也类似一个指针由Person Prototype -> 实例对象。
在原型中,还有一个属性是 constructor,他保存的是 Person这个构造函数的主体。由Person Prototype constructor -> Person。
在这里,还要明确几个问题
- 创建的实例对象都是独立的么?
创建的实例对象都是独立的,没有联系的,他们共享从父函数继承而来的属性和方法。
- 创建的实例对象通过
__proto__访问的原型是谁的?创建的实例对象通过proto访问的原型是父函数的(Person)。
- 通过构造函数创建的实例对象有什么特点?
他会继承来自构造函数的方法和属性,当方法和属性有重名时,优先使用自身的。
说到这里,再深入一步

这个图就比较复杂了,但是也很好理解。他就是上面所有东西的总结。当然还提出了一个新的概念:原型链
简单的说,就是当原型不断的向上溯源,这个过程是链式的,所以就有了原型链。
需要注意的一点就是:所有对象的原型,祖先就是 Object.prototype,当在向上查找时就是 null,所以在JS的基本数据类型中Null没有原型。
如何使用原型
了解了原型的概念,使用原型就能够干什么,我们需要总结一下
原型实例化
这也是我们上述说到的构造函数的形式。因为在我们的应用程序中,我们可能需要创建多个类似 Person 的实例,有 Lucy,tom,Jack 等等。而这些实例对象使用构函数上的原型提供的方法,这就是原型实例化模式,将函数本身称为“构造函数”,它负责构造一个个新的实例对象。
class Person {constructor(name, age) {// const this = Object.create(Person.prototype) 将原型传递给thisthis.name = name;this.age = age;// return this // 默认将this进行返回}sayName() {console.log(`Hello ${this.name} !`);}sayAge() {console.log(`your age is ${this.age}`);}toString() {this.age += 1;console.log(this.age);}}const Tom = new Person("tom", 18);const Jack = new Person("jack", 18);Tom.sayName(); // Hello tom !Jack.sayName(); // Hello jack !Tom.toString(); // 19
使用这种方式,我们创建出来的实例对象可以使用原型上的方法。避免了我们使用大量重复的方法干同样的事情,同时可以对原型上的方法进行重写,比如我们重写了 toString 方法。在上面的代码中,我们还注释了两行代码,其实那个就是构造函数内部做的事情,其中 Object.create 的含义就是:
Object.create(proto, [propertiesObject])
方法创建一个新对象,使用现有的对象来提供新创建的对象的 proto。
proto : 必须。表示新建对象的原型对象,即该参数会被赋值到目标对象(即新对象,或说是最后返回的对象)的原型上。该参数可以是 null, 对象, 函数的 prototype 属性 (创建空的对象时需传 null , 否则会抛出 TypeError 异常)。
propertiesObject : 可选。 添加到新创建对象的可枚举属性(即其自身的属性,而不是原型链上的枚举属性)对象的属性描述符以及相应的属性名称。这些属性对应 Object.defineProperties()的第二个参数。
返回值:在指定原型对象上添加新属性后的对象。
原型实例化有几个好处:
- 在原型上扩展方法,节省内存。
- 方便我们创建实例对象。
- 方便我们实现继承。
直接使用 JS 内置对象上原型的方法
ObjectFunctionArrayStringRegExpDateNumberBooleanSymbol- …
上述的标准内置对象中,都包含了原型上的方法的使用
继承
其实继承不是本文章的重点,网上也有很多关于实现继承的方式,大概上来说也就是那么几种,通过内存占用,传递参数,复杂程度等角度考虑去实现继承,其目的就是为了让子对象能够使用父对象的属性或方法。
比如:
上面的博客文章都已经对继承讲解的比较详细,如果感兴趣可以看看。
ES6关键字extends
在上面,我已经贴了很多的文章关于实现继承的方式。但是在这里还是要讲一下关于ES6实现继承的一些原理
class Person {constructor(name, age) {this.name = namethis.age = age}}class sayName extends Person {constructor(name, age, grade) {this.name = namethis.age = agethis.grade = grade}sayGrade(){console.log(`The grade is ${this.grade}`)}}
我们可以使用babel + babel-preset-es2015-loose进行转义:
function _inheritsLoose(subClass, superClass) {subClass.prototype = Object.create(superClass.prototype)subClass.prototype.constructor = subClasssubClass.__proto__ = superClass}var Person = function(name, age) {this.name = namethis.age = age}var Student = (function(_Person) {_inheritsLoose(Student, _Person)function Student(name, age, grade) {var _this// 组合继承_this = _Person.call(this, name, age) || this_this.grade = gradereturn _this}var _proto = Student.prototype_proto.sayGrade = function sayGrade() {// console.log()}return Student})(Person)
在这个里面,我们就能够清楚看见:整个ES6的extends实现的是原型继承+组合继承。
与原型相关的操作符
new
老是有人会问执行new操作符到底干了什么,我们可以从代码的角度去认识。
function isObject(value) {const type = typeof valuereturn value !== null &&(type === 'object' || type === 'function')}/*constructor: 表示new的构造器args: 表示给构造器传递的参数*/function New(constructor, ...args) {// new对象如果不是函数就会抛出异常if(typeof constructor !== 'function') throw new TypeError(`${constructor} is not a constructor`)// 创建一个target用于接收当前构造函数构造器的原型,在默认的情况下就是this, 这里使用target也是同样的意思const target = Object.create(constructor.prototype)// 将构造器的this指向上一步创建的空对象并执行,为了给this添加实例属性const result = constructor.apply(target, args)// 检查返回的结果是否为对象return isObject(result) ? result : target}
instanceof
这个操作符用于判断对象是否是某个类的实例,它的原理就是比较简单,就是通过比较两者的原型(也可以是多级原型)是否相等。
function isObject(value) {const type = typeof valuereturn value !== null &&(type === 'object' || type === 'function')}function instanceOf(object, constructor) {if (!isObject(constructor)) {throw new TypeError(`Right-hand side of 'instanceof' is not an object`);} else if (typeof constructor !== 'function') {throw new TypeError(`Right-hand side of 'instanceof' is not callable`);}// 关键return constructor.prototype.isPrototypeOf(object);}
需要注意的点
- 箭头函数无法作为构造函数使用,同时和
new搭配会发生错误,原因就是箭头函数内部没有this。 - 箭头函数没有
prototype属性。 - 创建的实例对象不一定需要使用
new才能使用prototype上的方法。我们可以使用call,apply,bind通过传递this使用原型上的方法。 - 使用
Object.getPrototypeOf获取实例对象的原型。也意味着Object.getPrototypeOf(Tom) === Person.prototype(Person是构造函数,Tom是根据Person创建出来的实例对象)。 - 我们可以使用
for in枚举原型上是否包含某一个属性。比如for(let key in Tom),此种方法会打印原型以及实例对象上的所有可枚举对象。 - 我们使用
hasOwnProperty可以帮助我们枚举创建的实例对象上的属性。 - 可以使用
instanceof检查对象是否是类的实例。Tom instanceof Person。
总结
总的来说,其实原型,原型链。本质上当我们使用到 prototype,他就是一个对象罢了,没什么复杂的,他们被提出来都是为了解决一些实际性的问题,为了方便使用 JS 面向对象,其实这里也是形似,在这里 JS 是都能够真的如同 Java 一样面向对象么?值得我们深思。我们更重要的是学习 JS 中面向对象编程的思想。如何去封装,去封装私有化,去继承,这个值得我们去学习和掌握。当然还有原型污染的问题,有兴趣的可以了解一下。
【参考资料】
