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(); // tom
Jack.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) 将原型传递给this
this.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 内置对象上原型的方法
Object
Function
Array
String
RegExp
Date
Number
Boolean
Symbol
- …
上述的标准内置对象中,都包含了原型上的方法的使用
继承
其实继承不是本文章的重点,网上也有很多关于实现继承的方式,大概上来说也就是那么几种,通过内存占用,传递参数,复杂程度等角度考虑去实现继承,其目的就是为了让子对象能够使用父对象的属性或方法。
比如:
上面的博客文章都已经对继承讲解的比较详细,如果感兴趣可以看看。
ES6关键字extends
在上面,我已经贴了很多的文章关于实现继承的方式。但是在这里还是要讲一下关于ES6实现继承的一些原理
class Person {
constructor(name, age) {
this.name = name
this.age = age
}
}
class sayName extends Person {
constructor(name, age, grade) {
this.name = name
this.age = age
this.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 = subClass
subClass.__proto__ = superClass
}
var Person = function(name, age) {
this.name = name
this.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 = grade
return _this
}
var _proto = Student.prototype
_proto.sayGrade = function sayGrade() {
// console.log()
}
return Student
})(Person)
在这个里面,我们就能够清楚看见:整个ES6
的extends
实现的是原型继承+组合继承。
与原型相关的操作符
new
老是有人会问执行new操作符到底干了什么,我们可以从代码的角度去认识。
function isObject(value) {
const type = typeof value
return 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 value
return 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
中面向对象编程的思想。如何去封装,去封装私有化,去继承,这个值得我们去学习和掌握。当然还有原型污染的问题,有兴趣的可以了解一下。
【参考资料】