对于搞前端的小伙伴来说,不论是老手还是老鸟,我想对于原型应该都被折腾过,总是云里雾里的感觉,要是原型都没搞明确,你还好意思说你是前端攻城狮?

对于对象

当一说到面向对象(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指向被切断了,指向了新的原型。

小结一下

默认状况下(因为原型对象实际上是可写的,因而能够被扭转):

  1. 任何函数都有一个指向其原型对象的指针属性prototype;
  2. 任何对象实例都有一个指向其构造函数原型对象的外部指针[[Prototype]](__proto__)
  3. 原型对象也是对象,因而也有__proto__(例如上图中指向Object.prototype那个);
  4. 对象实例的__proto__指针指向构造函数的原型对象:wangwu.__proto__ === Person.prototype
  5. 原型对象的constructor属性指向构造函数: Person.prototype.constructor === Person
  6. 构造函数和对象实例没有间接分割,仅仅是都有一个指针属性指向同一个原型对象。

对象实例辨认(检测)

咱们晓得,对于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。