前言
在我一开始学习 java web 的时候, 对 JS 就一直抱着一种只是简单用用的心态, 于是并没有一步一步地去学习, 当时认为用法与 java 类似, 但是在实际 Web 项目中使用时却比较麻烦, 便直接粗略了解后开始使用 jQuery. 但现如今, 前端发展迅速, JS 语法方便也有了相当大的改善, 并且伴随着 node.JS 的登场, JS 的适用性也更加广泛. 其实也是自己了解到了 electron 的存在, 再加上 Web 开发中前端与后端开发也比较密切, 于是这便又掉头回来重新开始学习 JS. 在学习的过程中, 仔细学习了一下 JS 的原型链, 也在这里做个记录, 如果有不对的地方, 还请各位指出, 本人感激不尽!
正文
在 JS 的世界中, 一切皆对象, 那么我们先将对象分为三类: 实例对象, 原型对象, 函数对象.
实例对象简单说就是通过构造函数所创建的对象.
函数对象好理解, JS 的函数本身也是个对象, 这个对象有这方法名, 参数, 方法体等属性. 构造函数是一种特殊的函数, 了解过其他 OOP 语言都知道, 构造函数往往会在实例对象创建的时候调用, 主要是用来完成实例对象的初始化操作. 但是在 JS 中, 构造函数与普通函数并不太大区别, 我们也可以像使用普通函数一样使用构造函数, 即不使用 new 关键字. 所以从本质上讲, 普通函数也是构造函数, 而构造函数只是从功能上区分的一个称呼, 体现在代码里就是用不用 new 关键字. 但为了接下来的说明, 下面将都会使用构造函数对象.
原型对象比较特殊, 现在先暂时记住通过实例对象与函数对象都能找到对应的原型对象.
这三类对象之间其实都有着联系, 而通过这些联系就形成了 JS 的完整的原型链. 我们接下来就按照这三类对象之间的关系来逐渐了解原型链.
实例对象与构造函数对象
首先来看实例对象与构造函数对象的联系. 通过 new 关键字, 我们可以通过构造函数得到一个实例对象. 例如:
- function Student(name){
- this.name = name;
- }
- var stu = new Student('wang');
在上面的片段中, Student 是一个构造函数, stu 则是一个通过 Student 创建的实例对象. 二者的联系很明显, 而在 JS 里则体现在实例对象 stu 的 constructor 属性中:
stu.constructor === Student; // true
那么反过来, 我们虽然不能通过构造函数对象直接找到它所有的实例对象, 但是可以通过 instanceof 关键字来判断一个对象是不是这个构造函数的实例对象:
stu instanceof Student; // true
原型对象与其他两类对象
上面我们也说了, 构造函数与普通函数没有什么区别, 那么直接使用构造函数, 那 this 自然是指内置全局对象 Windows. 但如果用 new,this 就指的是新的实例对象, 而且这个方法还会返回这个实例对象. 到这里大致就能猜到加了关键字 new 做了什么操作了, 它创建了一个新的空对象, 并且把构造函数中的 this 替换为空对象, 最后把这个对象返回.
那么为实例方法增加一个普通函数也这样做, 从结果来说是没有问题的:
- function Student(name){
- this.name = name;
- this.say = function(){
- console.log`I'm ${name}`;
- };
- }
- stu1 = new Student('wang');
- stu2 = new Student('li');
- stu1.say(); // I'm wang
- stu2.say(); // I'm li
- stu1.say === stu2.say; // false
但是我们会发现, stu1 与 stu2 的 say 函数对象竟然不是一个, 那就说明如果创建了 1000 个 Student, 就会有 1000 个 say 函数对象出现, 而这 1000 个 say 实现的功能完全一致, 这对内存而言显然是极大的浪费.
如何解决这个问题呢? 既然多个函数完全一致, 那么自然可以把这个函数对象放在一个地方, 当访问 stu1 和 stu2 的 say 函数时, 统一去拿这个地方的函数对象即可. 如果我们自己实现这个功能, 当在实例对象中使用函数对象时, 我们又得自己去手动去公共的地方寻找函数对象, 这么做显然太费劲了.
好在这些 JS 都已经帮我们做了, 每个构造函数对象都拥有一个 prototype 属性, 这个属性指向的是一个对象, 这个对象我们就叫它原型对象. 而这个原型对象又拥有一个与实例对象一样的 constructor 属性, 同样也是指向构造函数对象.
另外, 对于每个对象, 又都有一个__proto__的属性指向它的原型对象. 当我们访问一个对象的某个属性时, 实际上是先在当前对象寻找这个属性, 如果没有找到, 则会继续到__proto__所指的对象 (原型对象) 中寻找.
- function Student(name){
- this.name = name;
- }
- Student.prototype.say = function(){
- console.log`I'm ${name}`;
- };
- new Student('liu').say === new Student('zhang').say; // true
为方便理解, 这里再放一张图, 对照着这张图下面的代码就容易看明白了, 之后如果遇到不明白的也可以回过头来看图, 直观明了.
- var chen = new Student('chen')
- chen.__proto__ === Student.prototype; // true
- chen.constructor === Student; // true
- chen.constructor === Student.prototype.constructor; // true
深入
明白了上面这些概念, 我们把视角放大, 不再局限于 Student. 前面我们说到所有对象都有一个__proto__的属性, 那么对于函数对象和原型对象自然也不例外, 我们接下来的关注点就是这两类对象的__proto__属性.
首先来看函数对象. 在前面的代码中, Student 函数对象的__proto__是谁呢? 答案是 Function 的原型对象.
Student.__proto__ === Function.prototype; // true
一切皆对象, 那么 Function 的__proto__又是谁? 还是 Function 的原型对象:
Function.__proto__ === Function.prototype; // true
为什么? 因为 Function 也是个函数对象. 通常我们创建函数的方式为
- function xxx(x){
- ...
- }
- var yyy = function(y){
- ...
- };
那么其实还有一种写法:
- var zzz = new Function('z','...');
- // 例如:
- var hello = new Function('msg','console.log(msg)');
- hello('hi'); // hi
这样的写法显然能直接看出来, Function 是个函数对象. 于是便有一些有趣的事情了:
- Function.__proto__ === Function.prototype; // true
- Function.constructor === Function; // true
- Function.constructor === Function.prototype.constructor; // true
Function 是自己的函数对象, 也是自己的实例对象:
var Function = new Function(...);
至于为什么会这样, 这就比较像先有鸡还是先有蛋的问题了. 我们只需要知道所有函数对象 (包括 Function) 的__proto__都指向 Function 的原型对象.
与 Function 类似, Object 也是一个函数对象.(举一反三, Array,String,Number 等都是)
我们可以这样创建一个空的 Object:
var obj = new Object();
那么 Object 的原型对象的__proto__是谁呢? 是 null.
Object.prototype.__proto__; // null
之前说过, 当我们用. 操作符去拿一个属性时, JS 会先在当前对象里寻找, 没有的话去__proto__的对象 (原型对象) 里寻找. 那么如果__proto__(原型对象)里还没有, 就继续去它的__proto__里寻找, 以此重复. 那么什么时候是个头呢? 直到__proto__为 null 时.
我们知道所有对象都有 toString 方法, Student 的实例对象 stu 也是个对象, 但我们明显没有给它添加 toString 方法, 为什么它会有呢? 因为 stu 的__proto__最终指向的是 Object 的原型对象. 这也就是 JS 继承的本质了.
- stu.__proto__; // {
- constructor: ƒ
- }
- stu.__proto__.__proto__; // {
- constructor: ƒ, ..., toString: ƒ, ...
- }
- stu.__proto__.__proto__ === Object.prototype; // true
- stu.toString === Object.prototype.toString; // true
所以, 遍历所有对象的__proto__最终都会来到 Object 的原型对象.
来源: https://www.cnblogs.com/baka-sky/p/10092147.html