JavaScript原型详解
其实在此之前,关于原型的东西,都是零零散散,没有比较系统地去整理过。
鉴于之前在掘金看到一篇文章,总结得很好,然后就想自己地敲一遍,并一一验证,以加深印象和理解。(出自 ULIVZ,他的掘金主页)。
一、引入:普通对象与函数对象
在 JavaScript 中,一直有一种说法,万物皆对象。事实上,在 JavaScript 中,对象也是有区别的,我们可以将其划分为 普通对象
和 函数对象
。Object
和 Function
便是 JavaScript 自带的两个典型的 函数对象
。而函数对象就是一个纯函数,所谓的函数对象,其实就是使用 JavaScript 在 模拟类
。
那么,什么是普通对象,什么又是函数对象呢?
先创建三个 Function
和 Object
的实例。
function fn1() {};
var fn2 = function() {};
var fn3 = new Function('getName', 'console.log("Frankie")');
var obj1 = {};
var obj2 = new Object();
var obj3 = new fn1();
打印以下结果,可以得到:
console.log(typeof Object); // function
console.log(typeof Function); // function
console.log(typeof obj1); // object
console.log(typeof obj2); // object
console.log(typeof obj3); // object
console.log(typeof fn1); // function
console.log(typeof fn2); // function
console.log(typeof fn3); // function
在上述的例子中,obj1
、obj2
、obj3
为 普通对象
(均为 Object 的实例),而 fn1
、fn2
、fn3
为 函数对象
(均是 Function 的实例)。
如何区分呢?记住这句话就行了:
- 所有 Function 的实例都是函数对象,而其他的都是普通对象。
说到这里,细心的同学会发表一个疑问。文中开头,我们提到 Object
和 Function
均是 函数对象
,而这里又说:所有的 Function
的实例都是 函数对象
,难道 Function
也是 Function
的实例吗?(先留下疑问)
从图中可以看出,对象本身的实现还是要依靠构造函数,那
原型链
到底是用来干嘛的呢?
众所周知,作为一门面向对象的语言,必定具有以下特征:
- 对象唯一性
- 抽象性
- 继承性
- 多态性
原型链最大的目的就是为了实现继承。
二、进阶:prototype 与 __proto__
原型链究竟是如何实现继承的呢?首先,我们要引入介绍两兄弟:prototype
和 __proto__
,这是在 JavaScript 中无处不在的两个变量,然而,这两个变量并不是在所有的对象上都存在。
对象类型 | prototype | __proto__ |
---|---|---|
普通对象(NO) | ❎ | ✅ |
函数对象(FO) | ✅ | ✅ |
首先,我们先给出以下结论:
- 只有
函数对象
才具有prototype
这个属性; prototype
和__proto__
都是 JavaScript 在定义 一个函数或对象时自动创建的预定义属性
。
function fn() {};
console.log(typeof fn.__proto__); // function
console.log(typeof fn.prototype); // object
const obj ={};
console.log(typeof obj.__proto__); // function
console.log(typeof obj.prototype); // undefined,普通对象没有 prototype
也就是说,下面代码成立:
console.log(fn.__proto__ === Function.prototype); // true
console.log(obj.__proto__ === Object.prototype); // true
看起来很酷,结论瞬间被证明,感觉是不是很爽,那么问题来了:既然 fn 是一个函数对象,那么 fn.prototype.__proto__
到底等于什么?
这是我尝试去解决这个问题的过程:
- 首先用
typeof
得到fn.prototype
的类型:"object"
; - 既然是
"object"
,那fn.prototype
岂不是Object
的实例?我们验证一下:
console.log(fn.prototype.__proto__ === Object.prototype); // true
接下来,如果要你快速地写出,在创建一个函数时,JavaScript 对该函数原型的初始化代码,你是不是也能快速地写出:
// 实际代码
function fn() {};
// JavaScript 自动执行
fn.prototype = {
constructor: fn;
__proto__: Object.prototype
}
fn.__proto__ = Function.prototype;
到这里,你是否有一丝恍然大悟的感觉?此外,因为普通对象就是通过 函数对象
实例化(new
)得到的,而一个实例不可能再次进行实例化,也就不会让另一个对象的 __proto__
指向它的 prototype
,隐藏本节一开始提到的普通函数没有 prototype
属性的结论似乎非常好理解了。从上述的分析,我们还可以看出,fn.prototype
就是一个普通的对象,它也不存在 prototype
属性。
再回顾下上一节,我们还遗留了一个疑问:难道 Function
也是 Function
的实例?
是时候去掉 应该
让它成立了:
console.log(Function.__proto__ === Function.prototype); // true
三、重点:原型链
上一节,我们详解了 prototype
和 __proto__
。实际上,这两兄弟主要就是为了构造原型链而存在的。
先上一段代码:
function Person(name, age) {
this.name = name;
this.age = age;
} // 1️⃣
Person.prototype.getName = function() {
return this.name;
}; // 2️⃣
Person.prototype.getAge = function() {
return this.age;
}; // 3️⃣
var person = new Person("Frankie", 20); // 4️⃣
console.log(person); // 5️⃣
console.log(person.getName()); // 6️⃣
// 采用 ES6 更优雅的写法?哈哈
// Object.assign(Person.prototype, {
// getName() {
// return this.name;
// },
// getAge() {
// return this.age;
// },
// })
解析一下执行细节:
- 执行 1️⃣,创建一个构造函数
Person
,要注意前面已经提到,此时Person.prototype
已经被自动创建,它包含constructor
和__proto__
这两个属性; - 执行 2️⃣,给对象
Person.prototype
增加一个方法getName()
; - 执行 3️⃣,给对象
Person.prototype
增加一个方法getAge()
; - 执行 4️⃣,右构造函数
Person
创建一个person
实例,值得注意的是,一个构造函数在实例化时,一定会自动执行该构造函数。 - 在浏览器得到 5️⃣ 的输出,即
person
应该是:
{
name: 'Frankie',
age: 20,
__proto__: Object // 实际上就是 Person.prototype
}
结合上一节的经验,以下等式成立:
console.log(person.__proto__ === Person.prototype); // true
- 执行 6️⃣ 的时候,由于在
person
中找不到getName()
和getAge()
这两个方法,就会继续朝着原型链上查找,也就是通过__proto__
向上查找,于是很快在person.__proto__
中,即Person.prototype
中找到了这两个方法,于是停止查找并执行得到结果。
这便是 JavaScript 的原型继承。准确的说,JavaScript 的原型继承是通过 __proto__
并借助 prototype
来实现的。
于是,我们可以作以下总结:
- 函数对象的
__proto__
指向Function.prototype
; - 函数对象的
prototype
指向instance.__proto__
; - 普通对象的
__proto__
指向Object.prototype
; - 普通对象没有
prototype
属性; - 在访问一个对象的某个属性很方法时,若在当前对象上找不到,则会尝试访问
obj.__proto__
,也就是访问该对象的构造函数的原型objCtr.prototype
,若仍然找不到,会继续查找objCtr.prototype.__proto__
,像依次地查找下去。若在某一刻,找到该属性,则会立刻返回值并停止对原型链的搜索,若找不到,则返回undefined
。
为了检验你对上述的理解,请分析下面两个问题:
- 以下代码输出结果是什么?
console.log(person.__proto__ === Function.prototype); // false
Person.__proto__
和Person.prototype.__proto__
分别指向何处?
console.log(Person.__proto__ === Function.prototype); // true
console.log(Person.prototype.__proto__ === Object.prototype); // true
// 分析:
// 1. 前面已经提到过,在 JavaScript 中万物皆对象。Person 很明显是 Function 的实例,
// 因此,Person.__proto__ 指向 Function.prototype。
// 2. 因为 Person.prototype 是一个普通对象,因此Person.prototype.__proto__ 指向 Object.prototype。
// 3. 为了验证 Person.__proto__ 所在的原型链中没有 Object,
// 以及 Person.prototype.__proto__ 所在的原型链中没有 Function, 结合以下语句验证:
console.log(Person.__proto__ === Object.prototype); // false
console.log(Person.prototype.__proto__ == Function.prototype); // false
四、终极:原型链图
上一节,我们实际上还遗留了一个疑问:原型链如果一个搜索下去,如果找不到,那何时停止呢?也就是说,原型链的尽头是哪里?
我们可以快速地利用下面的代码验证:
function Person() {};
var person = new Person();
console.log(person.name); // undefined
很显然,上述输出 undefined
。下面简述查找过程:
person // 是一个对象,可以继续
person['name'] // 不存在,继续查找
person.__proto__ // 是一个对象,可以继续
person.__proto__['name'] // 不存在,继续查找
person.__proto__.__proto__ // 是一个对象,可以继续
person.__proto__.__proto__['name'] // 不存在, 继续查找
person.__proto__.__proto__.__proto__ // null !!!! 停止查找,返回 undefined
原来路的尽头是一场空。
最后,再回过头来看看上一节的那演示代码:
function Person(name, age) {
this.name = name;
this.age = age;
} // 1️⃣
Person.prototype.getName = function() {
return this.name;
}; // 2️⃣
Person.prototype.getAge = function() {
return this.age;
}; // 3️⃣
var person = new Person("Frankie", 20); // 4️⃣
console.log(person); // 5️⃣
console.log(person.getName(), person.getAge()); // 6️⃣
我们来画一个原型链图,或者说,将其整个原型链图画出来?请看下图:
画完这张图,基本上之前所有的疑问都可以解答了。
与其说万物皆对象,万物皆空似乎更加形象。
五:调料:constructor
前面已经有所提及,但只有原型对象才具有 constructor
这个属性,constructor
用来指向引用它的函数对象。
console.log(Person.prototype.constructor === Person); // true
console.log(Person.prototype.constructor.prototype.constructor === Person); // true
这是一种循环引用。当然你也可以在上一节的原型链图中画上去,这里就不赘述了。
六:补充 JavaScript中的 六大内置(函数)对象的原型继承
通过前文的论述,结合相应的代码验证,整理出以下原型链图:
由此可见,我们更加强化了这两个关掉:
- 任何内置对象(类)本身的
__proto__
都指向Function
的原型对象;- 除了
Object
的原型对象的__proto__
指向null
,其他所有的内置函数对象的原型对象的__proto__
都指向Object
。
console.log(Object.prototype.__proto__ === null); // true
六、总结
来几句短总结:
- 若
A
通过new
创建了B
,则B.__proto__ = A.prototype
; __proto__
是原型链查找的起点;- 执行
B.a
,若在B
中找不到a
,则会在B.__proto__
中,也就是A.prototype
中查找,若A.prototype
中仍然没有,则会继续向上查找,最终,一定会找到Object.prototype
,倘若还找不到,因为Object.prototype.__proto__
指向null
,因此会返回undefined
; - 为什么万物皆空,还是那句话,原型链的顶端,一定有
Object.prototype.__proto__
等于null
。
The end.
下一篇:如何用 JavaScript 实现类的继承呢?
未完待续...