在Brendan Eich设计Javascript时,借鉴了Self和Smalltalk这两门基于原型的语言。之所以选择基于原型的面向对象系统,是因为Brendan Eich一开始没有打算在JavaScript中加入类的概念,其设计初衷是为非专业的开发人员提供一个方便的工具,使其使用尽可能简单、易学。随着人们对网页要求的逐渐提高,对JavaScript这门语言的要求愈来愈高,开发人员也需要对这门语言有充足的了解。

中javascript中,对象的产生是通过原型对象而来的

原型的解释


原型:为其他对象提供共享属性的对象

每个对象都有一个__proto__(又名:[[Prototype]])属性, 该属性指向原型对象,并从中继承数据、结构和行为。每个函数(函数也是对象)都有一个 prototype 属性,它指向的也是一个原型对象。

这里的__proto__[[Prototype]]两个名词,在一些文章中常常混用在一起,导致难以理解,这里对两者进行解释。

__proto__译为隐式原型,又名[[Prototype]],它指向原型对象。官方 ECMAScript 规定了 prototype 是个隐式引用,但是民间浏览器开了口子,实现了一个属性 __proto__,让实例对象可以通过 __proto__ 访问其原型对象。再后来官方只好向事实低头,将 __proto__ 属性纳入规范中。至于 [[Prototype]],是在浏览器打印才显示的,它和 __proto__ 是一个含义,只是浏览器厂商换了个马甲。而且我们能在开发者工具中查看到的 [[Prototype]](或 __proto__ )是浏览器厂商故意渲染的一个虚拟节点。实际上并不存在该对象,所以 [[Prototype]] 属性既不能被 for in 遍历,也不能被 Object.key(obj) 查找出来。

查看对象的__proto__属性

const obj = {};
// target.__proto__、Object.getPrototypeOf(target)等可以访问对象的[[Prototype]]属性
console.log('obj', Object.getPrototypeOf(obj));

输出对象obj,查看其__proto__属性。

Object.getPrototypeOf(target)直接访问对象的__proto__属性

 查看函数的prototype属性

console.log('String', String.prototype);

 

 从 ECMAScript 6 开始,__proto__属性被废除(__proto__是一个访问器属性,getter 函数和setter函数),因为该属性可以这个改变对象的原型,这是一个非常慢且影响性能的操作,使用这种方式来改变和继承属性是对性能影响非常严重的。推荐使用Object.getPrototypeOf/Reflect.getPrototypeOf 和Object.setPrototypeOf/Reflect.setPrototypeOf(尽管如此,设置对象的 [[Prototype]] 是一个缓慢的操作,如果性能是一个问题,应该避免)。

小结

原型、原型对象(__proto__、[[Prototype]])、原型属性(prototype)其实是一个东西的不同称呼。当我们称呼这个东西为原型时,想表达的是它有什么作用,继承数据、结构和行为;当我们称它为原型对象时,是因为每个对象在其创建时会自带 __proto__/[[Prototype]] 属性,并指向这个对象的原型(对象);当我们称它为原型属性时,是因为每个函数都会在创建时自带 prototype 属性,而且这个属性是个指针,指向了原型对象。

原型链的解释

在上文中我们解释了每个对象都有 proto 属性,它指向原型对象。而原型对象是一个对象,所以原型对象也有自己的隐式引用(proto、[[Prototype]]),也有自己的原型对象,我们可以理解为“父类对象”。

“父类对象”的作用是:当你在访问目标对象一个属性时,如果目标对象内部不存在这个属性,就会循着目标对象的[[Prototype]] 属性所指向的原型对象上查找该属性,如果目标对象的原型对象上依然没有这个值,就会沿着原型对象的 [[Prototype]] 往它的父类对象上查找。以此类推,直到找到 null 为止。这一层层的追溯查找过程,就构成了原型链

[原型链:是 prototype 和 [[Prototype]] 的结合形成的产物。

下面我们通过一段代码来深入了解下原型链。

Object.create():方法用于创建一个新对象,使用现有的对象来作为新创建对象的原型(prototype)。接收两个参数:

  • proto:新创建对象的原型对象。
  • propertiesObject: 如果该参数被指定且不为undefined,则该传入对象的自有可枚举属性(即其自身定义的属性,而不是其原型链上的枚举属性)将为新创建的对象添加指定的属性值和对应的属性描述符。
const grandfather = {
    surname: '张',
    name: '张老三',
    gae: 66,
}
// 使用grandfather作为father的原型对象(即:[[Prototype]]或 __proto__)。
const father = Object.create(grandfather, {
    name: {
        value: '张三',
        enumerable: true,
        // writable (可写)特性指定是否可以设置属性的值。
        // enumerable (可枚举)特性指定是否可以在 fo/in 循环中返回属性的名字。
        // configurable (可配置)特性指定是否可以删除或者修改其特性。
    },
    age: {
        value: 43,
        enumerable: true,
    },
    somatotype: {
        value: 'strong',
        enumerable: true,
    }
});
// 使用father作为son的原型对象(即:[[Prototype]]或 __proto__)。
const son = Object.create(father, {
    name: {
        value: '张小三',
        enumerable: true,
    },
    age: {
        value: 20,
        enumerable: true,
    }
});

 

 

 查找原型

console.log('查找原型:', Object.getPrototypeOf(son) === father, Object.getPrototypeOf(Object.getPrototypeOf(son)) === grandfather);

通过Object.getPrototypeOf方法,向上查找确认son的原型对象指向为father,father的原型对象指向grandfather。

 

 

 自身属性

console.log('自身属性:', son.name, son.age);

访问son自身存在的属性时,将不再向上查找原型链上的属性。

构造函数

随着技术发展,开发者希望js能像标准的面向对象的语言一样,能够类来批量生产对象,于是通过构造函数来模拟其他语言里面的类。

上文提到,每个函数都有一个 prototype 属性,它指向的也是一个原型对象。所以一个对象是由构造函数实例化的,该实例对象的原型就是构造函数的prototype属性所指向的原型对象。

下面代码中通过构造函数Person实例化一个person对象,所以Person的prototype属性就是就是person对象的原型

function Person(name, age) {
    this.name = name;
    this.age = age;
}
const person = new Person('Tom', 25);
console.log('Person.prototype:', Person.prototype);
console.log('person.__proto__:', person.__proto__ === Person.prototype);
console.log('Object.getPrototypeOf():', Object.getPrototypeOf(person) === Person.prototype);

上图中原型对象中的constructor属性又是什么?

constructor:默认情况下,所有的原型对象都会自动获得一个 constructor(构造函数)属性,这个属性(是一个指针)指向 prototype 属性所在的函数(示例中Person)。

以当我们想知道一个对象是由什么构造函数实例化的时候,我们可以访问该对象的constructor属性,最终向上查找调用了对象原型上的constructor获取其构造函数。

下面代码中我们验证我们日常中声明的对象其构造函数是什么?

const arr = [];
const obj = {};
const func = () => {};
console.log('arr constructor:', arr.constructor, arr.constructor === Array);
console.log('obj constructor:', obj.constructor, obj.constructor === Object);
console.log('func constructor:', func.constructor, func.constructor === Function);

最终我们可以的得出结论,直接声明一个对象(例arr = [])或者通过构造函数实例化(newArr = new Array())是等效的。

  

 

 

 小结

前面已经介绍了原型、原型链、和构造函数,下图着重记录了这三者之间的关系,且总结出以下结论:

  • JavaScript中每个对象都有一个原型对象。可以通过Object.getPrototypeOf(target)方法来访问对象的原型对象。
  • 构造函数的prototype属性指向一个对象,这个对象是该构造函数实例化出的对象的原型对象。
  • 原型对象的constructor属性也指向其构造函数。
  • 实例对象的constructor属性是从它的原型上访问到的。

 

 

 问: 原型是什么

  • 每个对象都有一个__proto__属性,该属性指向对象自己的原型对象,并从原型对象中继承数据和方法。
  • 每个构造函数都有一个prototype属性,该属性指向实例化对象的原型对象。
  • 每个构造函数的prototype属性所指向的原型对象里的constructor属性指向构造函数本身。

问: 原型链是什么

  • 每个对象都有自己的原型对象,原型对象本身也是对象,所以原型对象也有原型对象,从而连接成了一条原型链。

完整原型链

function Person(name, age) {
    this.name = name;
    this.age = age;
}

console.log('Object.getPrototypeOf(Array):', Object.getPrototypeOf(Array));
console.log('Object.getPrototypeOf(Function):', Object.getPrototypeOf(Function));
console.log('Object.getPrototypeOf(Object):', Object.getPrototypeOf(Object));
console.log('Object.getPrototypeOf(Number):', Object.getPrototypeOf(Number));
console.log('Object.getPrototypeOf(String):', Object.getPrototypeOf(String));
console.log('Object.getPrototypeOf(Boolean):', Object.getPrototypeOf(Boolean));
console.log('Object.getPrototypeOf(Person):', Object.getPrototypeOf(Person));

 

 

 

尝试输出了常见构造函数以及我们自己声明的构造函数的原型对象是什么,结果发现:无论是JavaScript内置还是自定义的构造函数,其原型对象都是同一个对象——构造函数原型对象

此时我们完善下我们的关系图:

 

 

 原型对象的原型

console.log('Person.prototype.__proto__', Person.prototype.__proto__, Person.prototype.__proto__ === Object.prototype);
console.log('person.__proto__.__proto__', person.__proto__.__proto__, Person.prototype.__proto__ === Object.prototype);
console.log('===', Person.prototype.__proto__ === person.__proto__.__proto__);
// 因为属性方位链路较长,这里还是使用__proto__使得代码更为清晰。

通过输出构造函数Person.prototype的原型以及实例化出的对象person.__proto__的原型,并推断原型对象也是对象,那么原型对象的原型应该都是Object.prototype所指向的原型对象。那么最后Object.prototype还有原型吗?

 

此时我们完善下我们的关系图:

 

 构造函数原型对象的原型

在关系图中,左侧通过不断访问实例对象原型的原型链,我们找到了其终点是null,那么构造函数原型的原型链终点会不会也是null。

结合前面已经了解的知识,首先Object.prototype指向的原型对象说明其构造函数为Object,而构造函数原型对象的原型应该是Object.prototype,通过代码验证如下。

 console.log('Object', Object.prototype.constructor === Object);
 console.log('Person.__proto__.__proto__', Person.__proto__.__proto__ === Object.prototype);

 

 并根据验证结果,继续完善关系图:

 

 到此为止我们构造函数Object的原型(proto)是谁,构造函数原型对象的constructor指向谁,对于这一块作者目前没有看到很好的解析,所以只能把通用的关系图找到,如果有读者知道麻烦评论下,以供作者补全。

 

 

目前根据关系图总结原型链的规律:

  • 函数对象的prototype属性所指向的原型对象上的构造函数指向函数对象本身,即prototype和constructor在一起。
  • 关系图中存在两个三角关系,实例对象——原型对象——构造函数,Object——构造函数原型——Object.prototype,这两个三角关系有利于理解记忆关系图。
  • 顺着原型链向上查找,最终都会指向null。

获取函数原型链上的所有原型对象

let aaa = []
function prot(obj) {
    if (obj != null && obj !== undefined) {
        prot(obj.__proto__);
    }
    if (obj != null && obj !== undefined) {
        try {
            obj[Symbol.toStringTag] ? aaa.push(obj[Symbol.toStringTag]) : console.log(obj);
        } catch (e) {
            console.log(e)
        }
    }else {
        aaa.push(null)
    }

}

prot(Window.prototype)
console.log(aaa);