对于搞前端的小伙伴来说,不论是老手还是老鸟,我想对于原型应该都被折腾过,总是云里雾里的感觉,要是原型都没搞明确,你还好意思说你是前端攻城狮?
对于对象
当一说到面向对象(Object-Oriented OO)时,你第一反馈必定想到类、对象、接口实现等概念,那咱们这里为啥已上来就说对象呢?因为ECMAScript里没有类,另外因为ECMAScript中的函数没有签名,所以也没有接口。
ECMAScript-262中对象定义为:“无序属性的汇合,其属性能够是根本值、对象或者函数”。因而从数据结构的角度,能够把对象看成散列表(Hash Table)。
对象分类
从对象的创立形式上能够把对象分成:内置对象、宿主对象、自定义对象三大类。对于对象分类具体点这里。
特地须要强调的是,除了number、string、boolean、null、undefined、symbol这6中根本类型外,其它通通都是对象(援用类型),包含函数,所有的函数都是对象,反之则不成立。
对象和函数的关系
对象的创立
后面说过,ECMAScript中没有类,那怎么创建对象呢?
对象字面量
// 形式一: 对象字面量var zhangsan = { type: "人类", name: "张三", age: 18, greeting: function() { console.log(`hello I'am ${this.name}`); }};zhangsan.greeting(); // "hello I'am 张三"
该形式次要有一下几个问题:
- 当要创立多个变量的时候,不得不写大量反复代码;
- 每个实例都会持有一个greeting函数,但实际上性能都一样,没有复用,浪费资源;
- 创立所有“人类"(type="人类")的实例,type的值都是一样的,然而每个实例还是持有一个独立的正本;
- 创立实例无奈辨认类型(也就是说创立的实例具体是啥类型不晓得,只晓得它是Object的实例)。
工厂模式
// 形式二: 工厂模式function createPerson (name, age) { var p = new Object(); p.type = "人类"; p.name = name; p.age = age; p.greeting = greeting; return p;}var lisi = createPerson ("李四", 20);lisi.greeting(); // "hello I'am 李四"function greeting () { console.log(`hello I'am ${this.name}`);}
形式二尽管进行了封装,防止了创立时大量反复的代码,也通过把greeting抽离到全局作用域而解决了多个实例持有多个greeting正本的问题,但同时也给全局空间引入了一个只有该类型实例才会援用的函数,净化了全局空间;最初它也米有解决对象辨认问题。
// 形式三: 构造函数function Person (name, age) { this.type = "人类"; this.name = name; this.age = age; this.greeting = greeting;}var wangwu = new Person("王五", 24); // wangwu instanceof Person === truewangwu.greeting(); // "hello I'am 王五"function greeting () { console.log(`hello I'am ${this.name}`);}
这个形式近乎完满了,解决了对象辨认问题,然而任然没有解决共享函数净化全局空间的问题;为了解决这个问题,上面请出咱们的配角prototype(原型)。
原型&原型链
终于切入正题了,要解决下面形式三面临的问题,就要有一个属于构造函数专有(不必定义到全局净化全局空间),可能为构造函数创立的所有对象实例所共享的对象。这个对象就是原型(或称为原型对象)。
什么是原型(prototype)
默认状况下,任何函数都有一个属性prototype
,它是一个指针,指向一个对象(原型对象),原型对象的用处是蕴含特定类型实例所共享的属性和办法,默认原型对象只有一个constructor
属性,咱们能够给它定义更多属性和办法。
// 形式四: 原型法function Person (name, age) { this.name = name; this.age = age;}Person.prototype.type = "人类";Person.prototype.greeting = function () { console.log(`hello I'am ${this.name}`);};var wangwu = new Person("王五", 24); // wangwu instanceof Person === truewangwu.greeting(); // "hello I'am 王五"
那下面的实例wangwu是怎么找到原型对象里定义的greeting的呢?起因是所有的对象都有一个外部指针,指向实例构造函数的原型对象,ECMAScript-262第5版中称为[[Prototype]]
,尽管规范并没有定义怎么拜访这个外部指针,然而Firefox、Safari、Chrome在每个对象上都反对一个指向雷同、名为__proto__
指针属性。
在chrome console里查看wangwu的属性如下图:
[站外图片上传中...(image-1d07-1644313611733)]
原型链查找
当对象实例拜访某个属性或调用某个办法时,首先在自有属性里找,找到则返回值或发动调用,没有则沿着__proto__
的指向往上找,直到最初查到Object.prototype,任然没有查到,即终止并报错。
对象实例、构造函数、构造函数的原型对象这三者的关系如下图:
上图中红色的门路及为查找方向,这条有__proto__
指针串起来的链即为原型链(prototype chain)
。原型链的实质是一串程序指向原型对象的指针列表。
原型的动态性
因为对象实例的__proto__
仅仅是一个指向原型对象的指针,因而对原型对象的批改立刻能够在实例上体现进去,哪怕这个实例在批改原型之前创立的:
Person.prototype.work = function () { console.log('work function');}// 这里的wangwu是下面创立的实例,给原型减少work办法后,能够立刻调用wangwu.work(); // "work function"
然而如果重写整个原型对象后,相当于为构造函数指定了新的原型对象,而已创立的实例的__proto__
依然指向旧原型对象,因而拜访不到在新原型里定义的办法:
Person.prototype = { work: function () { console.log('work function'); }};// 报错wangwu.work(); // "wangwu.work is not a function"// 在批改原型对象后创立的实例,因为获取到的__proto__属性是指向新原型的,因而不会报错var sanma = new Person('三毛', 30);// 能够欢快的“工作”sanma.work(); // "work function"
[图片上传失败...(image-b3adea-1644313611733)]
笼罩整个原型对象后,相当于下面图中原来的prototype指向被切断了,指向了新的原型。
小结一下
默认状况下(因为原型对象实际上是可写的,因而能够被扭转):
- 任何函数都有一个指向其原型对象的指针属性prototype;
- 任何对象实例都有一个指向其构造函数原型对象的外部指针
[[Prototype]](__proto__)
;- 原型对象也是对象,因而也有
__proto__
(例如上图中指向Object.prototype那个);- 对象实例的
__proto__
指针指向构造函数的原型对象:wangwu.__proto__ === Person.prototype
;- 原型对象的
constructor
属性指向构造函数:Person.prototype.constructor === Person
;- 构造函数和对象实例没有间接分割,仅仅是都有一个指针属性指向同一个原型对象。
对象实例辨认(检测)
咱们晓得,对于number、string、boolean、undefined、function这几种类型值,能够通过typeof操作符简略辨别,然而对于除function外的援用类型实例和null,typeof都返回"object",然而再往细了辨别,某个对象实例是神类型的实例,typeof就没方法了。
instanceof操作符
要辨认具体的对象实例类型,就要用到instanceof操作符,格局为 instance instanceof Func
, instance是待检测实例对象,Func是一个构造函数,有了下面原型链的了解,那instanceof的检测机制就简略多了,只有在instance的原型链上某个__proto__
指向了Func的原型对象,就返回true,否则返回false。即:
instance.__proto__...__proto__ === Func.prototype
另外也能够用Func.prototype.isPrototypeof(instance)、Object.getPrototypeof(instance) === Func.prototype来判断。
console.log(wangwu instanceof Person); // trueconsole.log(wangwu instanceof Object); // trueconsole.log(Person.prototype.isPrototypeof(wangwu)); // trueconsole.log(Object.prototype.isPrototypeof(wangwu)); // trueconsole.log(Object.getPrototypeof(wangwu) === Person.prototype); // trueconsole.log(Object.getPrototypeof(wangwu) === Object.prototype); // false, 因为getPrototypeof函数只返回实例原型,而不会返回原型链上的其它原型
原型继承
了解了原型,那原型继承就很简略了,须要扩大的类指向父类的原型即可,上面是简略的原型继承实现:
function Men() { // }Men.prototype = Object.create(Person.prototype);Men.prototype.constructor = Men;
特地留神,给prototype属性赋值后,Men.prototype.constructor指向了Person,因而必须再把它指回Men。