原型对象 prototype
包含:
- 函数和原型 (function & prototype)
- 对象和隐式原型 (object & __proto__)
- 原型和隐式原型 (prototype & __proto__)
- 原型和构造器(prototype & constructor)
- 原型链 (prototype chain)
- 函数和对象间的关系(function & obj)
1.函数和原型 (function & prototype)
每一个函数都有一个属性 prototype,表示 "原型对象",以下简称原型。
1.1 原型 prototype 的来由
这个原型是怎么来的呢?
JS 不像其他静态语言,它没有"子类"和"父类"的概念,也没有"类"(class)和"实例"(instance)的区分,全靠一种很奇特的"原型链"模式,来实现继承。
这里就要提到当初 js 的创始人 Brendan Eich,他在创建 js 的时候遇到一个难题:要不要设计"继承"机制?
我们知道当初 Brendan Eich 在创建 JS 的时候,仅仅是想做简单的脚本语言以实现 "表单验证" 功能,那么它就无需 "继承" 机制,但 JS 里又必须有一种机制将所有对象联系起来。
最后, Brendan Eich 不打算引入 "类" 的概念,因为一旦有了 "类", JS 就变成了面向对象的编程语言了,会加大初学者的入门难度。因此他根据 C++ 和 Java 做了一个简化,把 new
命令引入到了 JS ,用来从原型对象生成一个实例对象,但在 JS 中 new
后面跟的不是一个类,而是一个构造函数。
回忆一下有见过哪些通过 new
出来的对象吗?类似于像 new Date( )
、new Array()
、new Set()
之类的都是由函数创建出来的对象。
例如前面的 Cat 的构造函数,就表示 "猫对象" 的原型(模版)。对 Cat 这个构造函数使用 new
关键字就会生成猫的实例 cat1、cat2... 等等。
在 JS 中对象的产生是通过原型对象而来,每一个对象,都有一个原型对象。而原型对象上面也有一个自己的原型对象,一层一层向上找,最终会到达 null。
1.2 原型 prototype 的作用
我们常说 JS 是一门基于原型的语言,所以 JS 是通过函数,去模拟类。
回顾通过工厂模式和 class 的形式去封装类时,我们是如何去处理“方法共享”这个问题的?
没错,将方法放在 prototype 身上。所以 prototype 的作用其实就是 给所有实例提供公共访问。
let obj = { name: "zhangsan" }; console.log(obj.name, obj.age); // zhangsan undefined Object.prototype.age = 18; console.log(obj.name, obj.age); // zhangsan 18
对象 obj 自身没有 age 属性,所以访问 obj.age 必定会得到 undefined。但 obj 可以看作通过 new Object() 创建的,或者说 obj 是 Object 类的实例,所以当在 Object.prototype 上放置 age 属性并且赋值后,实例对象自身没有的属性或值,通过访问创建该对象的类的 prototype,也能被访问到。
但通常,对象属性的值不会相同,比如每个人的姓名、年龄、性别。但方法却是相同或相似。所以为了不造成内存的浪费,只会将公共的方法放在类的 prototype 上。这也就是为什么我们自定义的 let students = []
或者 let users = []
这样的数组都能使用 push()
、length
之类的属性和方法了。
尝试在控制台打印 Array.prototype 你会发现数组所有的属性与方法。
函数对象与普通对象
2. 对象和隐式原型 (object & __proto__)
每一个实例对象都有一个 __proto__ 属性称为 隐式原型,指向创建该对象的构造函数的 prototype
。这个属性本身也是对象。
let arr = []; console.log(arr.__proto__); console.log(Array.prototype); console.log(arr.__proto__ == Array.prototype); // true
⚠️ 分清楚:
- 函数的属性
prototype
- 对象的属性
__proto__
3. 原型和隐式原型 (prototype & __proto__)
console.log(arr.__proto__ === Array.prototype); // true
4. 原型和构造器(prototype & constructor)
每一个函数的 prototype 上有 constructor 属性,指向原型所在的类。
5. 原型链(prototype chain)
提到“链”,我们能想到关于链的:
- 链式调用:美化代码,语义化的调用以便于偷懒
- 作用域链:找变量
- 原型链:找公用的属性。方法属于特殊的属性,属性所对应的值是一个函数。(这也就是为什么一个自定义的数组在没有显式设置属性方法,却也能使用
.push()
.length
等属性和方法的原因)
5.1 原型链的概念
一个实例对象,在调用属性或方法时,会依次从实例本身 --> 创建该实例对象的构造函数原型 --> Object.prototype ,查看是否有对应的属性或方法。这样的寻找方式就好像一个链条一样,从实例对象本身一直找到 Object.prototype ,专业上称之为原型链。
function Dog() { } Dog.prototype.eat = function () { console.log("eat meat!"); } let dogObj = new Dog(); dogObj.eat(); // eat meat!
为什么实例对象 dogObj 自身没有 eat 方法,但调用 eat 方法却能打印?原因就在于 eat 方法是放在实例对象 dogObj 的构造函数 Dog 的原型 prototype 上的。在实例中没找到的方法,在构造函数的原型上找到了:
dogObj.__proto__ === Dog.prototype; // true dogObj.__proto__.eat === Dog.prototype.eat // true // 实际调用的是:dogObj.__proto__.eat 也就是 Dog.prototype.eat
假设我需要调用一个 dogObj 没有的方法,Dog 也没有的方法呢?比如…toString()?
dogObj.toString(); // [object Object] /* 为什么执行成功? 在实例 dogObj 中没有定义该方法 在构造函数 Dog 的原型 prototype 上也没找到 在构造函数的原型的原型 Object 上找到了 实际调用的是 dogObj.__proto__.__proto__.toString 也就是 Object.prototype.toString */
由此可以看出 __proto__
就像一个链条,串联起了实例对象和原型。
但同时一个新的问题出现,凭什么 Dog.prototype.__proto__ (或者说 obj.__proto__.__proto__)是 Object.prototype 呢?
5.2 推测实例的原型链
来看一个推测步骤:
- 先找到实例的 __proto__ 的上一个对象,即 Dog 类的 prototype
- 判断 Dog.prototype 的类型,
typeof Dog.prototype // object
- 从上得出的结果是 object ,object 必定是由 Object 创建的,即:
Dog.prototype.__proto__ === Object.prototype
5.3 推测函数的原型链
上面提到从实例推导出了 Object,那函数呢?Function.__proto__
又是什么?
- 判断 Function 类型,
typeof Function // function
- 从上得到结论:函数类型的构造函数就是 Function,或者说函数都是由 Function 创建的
- 所以
Function.__proto__ === Function.prototype
或者说Dog.__proto__ === Function.prototype
同理可得所有可以通过 new 的构造函数的隐式原型,都是 Function 的原型:
- String.__proto__ === Function.prototype
- Number.__proto__ === Function.prototype
- Boolean.__proto__ === Function.prototype
- Date.__proto__ === Function.prototype
- Array.__proto__ === Function.prototype
- Object.__proto__ === Function.prototype
- Set.__proto__ === Function.prototype
- Map.__proto__ === Function.prototype
- RegExp.__proto__ === Function.prototype
5.4 原型链上不封顶?
刚才的推导里有一张图:
Object.prototype.__proto__ 该是谁?按照常理去推导,Object.prototype.__proto__
是 Object.prototype,但这样下去原型链就在 Object 处无限循环了。为了解决这个问题,Brendan Eich 就直接规定了 Object.prototype.__proto__
为 null
,打破了原型链的无限循环。
这样就形成了一个链条称为原型链。原型链的顶端通往 null。
- 本质上来说,所有 function 都可以看作是由
new Function()
生成的,所有通过new Function()
创建的对象叫做 函数对象。 - 所有对象都是由函数对象创建的。
- 函数属于对象,函数可以创建对象。
5. 原型链(prototype chain)
let Fn = function () {} Fn.prototype.talk = function () {console.log(123)} let obj = new Fn(); obj.talk(); // 实例对象 obj 自身是没有 talk 方法的,却可以通过 prototype 访问到构造函数的方法
6. 函数和对象间的关系 运算符 instanceof
instanceof
,单词表示 “实例”、“XX之间的关系”,instanceof
用于判断一个对象 是否是 一个构造函数的 实例对象。原理就是利用了原型链。注意,instanceof 只能用于复杂数据类型。
所有对象 instanceof Object 都得到 true,因为 Object 在原型链的上端。
语法:obj instanceof class
function Student(){} let obj = new Student(); console.log(obj instanceof Student); // true function Fn() {} let obj = {}; console.log(obj instanceof Fn); // false function fn(){} console.log(fn instanceof Object); // true console.log(fn instanceof Function); // true
总结
- 原型:每个函数都有一个 prototype 属性叫做原型
- 隐式原型:每个对象都有一个 __proto__ 属性叫做隐式原型
- 所有对象本质上都可以看作是通过 Object 创建的
- 所有函数本质上都可以看作是通过 Function 创建的
- 判断 obj 是不是一个构造函数的实例对象 xxx.instanceof function