博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
妈妈再也不用担心我的面试了 之 js原型和原型链
阅读量:5810 次
发布时间:2019-06-18

本文共 7458 字,大约阅读时间需要 24 分钟。

前言

最近在复习js基础,因为工作以后基本上没用过,天天都是拿起框架加油干,确实大部分都忘了。到了原型和原型链这一部分,觉得自己理解的比较模糊。又翻阅了《你不知道的javascript》、阮一峰老师的还有网络上的各种文章,收获满满(感谢各位作者大佬)。所以整理成这篇文章,加深自己的印象,也希望对大家有所帮助。

文章收录在作者代码库,主要是个人学习的代码以及文章,觉得有帮助可以点个小星星,会持续更新。

另外也希望大家可以支持一下我的开源作品,这是。感谢!

思维导图

不太了解原型链的同学可能会觉得有点乱,没关系,看完文章再回过头来看,就很清晰了。

Prototype

众所周知,在JavaScript中,可以通过关键字new调用构造函数来创建一个实例对象。

function Person(name){        this.name = name;        this.say = function () {            console.log(this.name);        }    }    let lisi = new Person('lisi');    let liwu = new Person('liwu');    lisi.say() // lisi    liwu.say() // liwu    console.log(lisi.say === liwu.say); // false复制代码

可以看出,lisi和liwu都有say这个方法,但是这两个方法并不是同一个。也就是说在创建对象的时候,每个实例对象都会有一套自己的属性和方法。很显然,这样造成了资源浪费。

这时候我们想,如果可以让实例对象引用同一个属性或方法就好了。所以JavaScript的作者引入了原型对象[Prototype]来解决这个问题。原型对象上有两个默认属性,constructor__proto__(下文会详细讲)。

function Person(name){        this.name = name;    }    Person.prototype.say = function () {        console.log(this.name);    }    let lisi = new Person('lisi');    let liwu = new Person('liwu');    console.log(lisi.say === liwu.say); // true    console.log(lisi.hasOwnProperty('say'), liwu.hasOwnProperty('say')); // false false复制代码

这个时候可以看到,构造的新的实例对象都有say方法,但是hasOwnProperty('say')返回的结果却是false。这说明实例对象自身是没有say方法的,之所以可以使用.say的方式来调用,是因为在使用.语法调用对象方法的时候会触发对象自身的[get]操作。

[get]操作会优先查找自身的属性,没有找到则会通过原型链来逐级查找上级的原型对象,直到js顶层的Object对象。所以此处可以说明实例对象会继承构造函数的原型对象上的属性和方法。

但是正因为如此,我们需要注意的是:因为原型对象的属性和方法是会被所有实例对象继承的,所以使用的时候要慎重考虑该属性或方法是否适合放在原型对象上。比如Person有一个age属性:

Person.prototype.age = 18;    console.log(lisi.age, liwu.age); // 18 18    Person.prototype.age = 20;    console.log(lisi.age, liwu.age); // 20 20复制代码

因为age属性是引用的Person的原型对象上的,所以原型对象上的属性值改了,所有的实例对象相应的属性值都会改动。这时候我们就不得不考虑,是否有必要将age属性放在原型对象了,毕竟鲁迅曾经说过:‘每个人都是都一无二的’。

强行插图,哈哈哈!我们再来看下面这种情况:

lisi.say = function() {        console.log('oh nanana');    };    lisi.say(); // oh nanana    liwu.say(); // liwu    console.log(lisi.hasOwnProperty('say'), liwu.hasOwnProperty('say')); // true false复制代码

这是为什么呢,其实和之前类似,是因为.语法在赋值的时候触发了对象的[set]方法,所以会给lisi自身加上一个say方法。而在调用方法时,最先找到自身的say方法调用,输出oh nanana。因为操作都是在lisi这个对象本身,所以对liwu没有影响。

constructor

constructor 即为 构造函数,构造函数其实和普通的函数没有什么区别,对构造函数使用new运算符,就能生成实例,并且this变量会绑定在实例对象上。

对于Person来讲,会有prototype属性指向它的原型对象,而在Person.prototype上又有constructor属性指向它对应的构造函数,所以这是一个循环的引用。大概是这样:Person -> Person.prototype —> Person.prototype.constructor -> Person

console.log(Person.constructor === Person.prototype.constructor) // true    console.log(Person.prototype.constructor === Person) // true    console.log(Person.hasOwnProperty('constructor')) // false复制代码

从这里可以看出,Person自身是没有constructor属性的,之所以可以使用,是因为从它的原型对象上继承了constructor属性。

延用上面的栗子,我们在加点东西:

function Chinese() {        this.country = '中国';    }    Person.prototype = new Chinese();    let lisisi = new Person('lisisi');    console.log(lisi.country, lisisi.country); // undefined  中国复制代码

在这个栗子中,我们将Person.prototype整体赋值成了Chinese的实例对象。注意,是赋值的实例对象,不是构造函数。上面打印结果是lisisi有country属性,这个我们好理解,因为lisisi继承了Person.prototype,而Person.prototype被我们赋值成了Chinese的实例对象,自然会继承Chinese实例对象的country属性。

但是lisi为什么没有country属性呢,之前改得say方法明明受影响啊。我们打印出lisi和lisisi的完整结构来看一下:

可以看到,其实是因为我们将Person.prototype整体替换成了Chinese实例对象,相当于改变了Person.prototype的地址,但是lisi在实例化的时候,引用的是之前的Person.prototype地址,这两者之间没有联系,自然不会有影响。而之前的say方法是用Person.prototype.say的形式改的,lisi继承的依旧是同一地址上的say方法,所以会受影响。

这个例子之所以放在这里讲,而不是prototype那里,是因为这个方法会有一点副作用,将Person.prototype整体赋值成了Chinese的实例对象,会导致原来的constructor属性也被覆盖掉。

console.log(lisisi instanceof Person); // true    console.log(Person.prototype.isPrototypeOf(lisisi)); // true    console.log(Object.getPrototypeOf(lisisi)); // Chinese {country: "中国"}        // instanceof做的事是判断在`lisisi`的整条[Prototype]链中是否有指向 Person.prototype 的对象。    // isPrototypeOf做的事是判断在`lisisi`的整条[Prototype]链中是否出现过 Person.prototype。    // 它们的区别在于前者要访问构造函数,后者直接访问原型对象。        console.log(lisisi.__proto__ === Person.prototype); // true       // __proto__指向实例对象对应的原型对象,但不一定是其构造函数的原型对象,因为prototype可以修改        console.log(lisisi.constructor === Chinese); // true    复制代码

从上可以看出,虽然lisisi继承的依然是的Person.prototype,但是由于Person.prototype指向了Chinese的实例对象。所以,这个时候lisisi的constructor已经不是Person了,而是继承了Chinese实例对象的constructor,也就是构造函数Chinese。为了解决这个问题,我们需要手动修正constructor的指向。

Person.prototype = new Chinese();    Person.prototype.constructor = Person;    let lisisi = new Person('lisisi');    console.log(lisisi.constructor === Person); // true复制代码

从这个栗子也可以说明,使用引用类型的constructor是并不安全的,因为他们可以修改。不过基础类型的constructor都是只读的,都指向对应基础类型构造函数。

let a = 'oh nanana', b = 0, c = true;    console.log(a.constructor, b.constructor, c.constructor);    // ƒ String() { [native code] } ƒ Number() { [native code] } ƒ Boolean() { [native code] }        a.constructor = {};    b.constructor = {};     c.constructor = {};    console.log(a.constructor, b.constructor, c.constructor);    // ƒ String() { [native code] } ƒ Number() { [native code] } ƒ Boolean() { [native code] }复制代码

proto

实例对象有__proto__ 属性,指向实例对象对应的原型对象,即lisi.__proto__ === Person.prototype。但是直接用.__proto__的写法来设置原型对象的写法是不被赞同的,因为这样还会有除了性能消耗以外的问题。MDN中这样说到:

由于现代 JavaScript 引擎优化属性访问所带来的特性的关系,更改对象的 [[Prototype]]在各个浏览器和 JavaScript 引擎上都是一个很慢的操作。其在更改继承的性能上的影响是微妙而又广泛的,这不仅仅限于 obj.proto = ... 语句上的时间花费,而且可能会延伸到任何代码,那些可以访问任何[[Prototype]]已被更改的对象的代码。如果你关心性能,你应该避免设置一个对象的 [[Prototype]]。相反,你应该使用 Object.create()来创建带有你想要的[[Prototype]]的新对象。

在《你不知道的JavaScript》中说到,__proto__的本质其实更像是getter/setter,大致实现为:

Object.defineProperty( Object.prototype, "__proto__", {        get: function() {            return Object.getPrototypeOf( this );        },        set: function(o) {            // ES6 中的 setPrototypeOf(obj, prototype) 设置原型对象            Object.setPrototypeOf(this, o );            return o;        }    } );复制代码

何为原型链

现在我们知道,实例对象的__proto__属性指向其对应的原型对象。而在原型对象prototype上又有constructor__proto__属性,此时的__proto__又指向上级对应的原型对象,最终指向Object.prototype, 而Object.prototype.__proto__ === null。这就构成了原型链,而原型链最终都是指向null。

还是来看个栗子:

function Person(name){        this.name = name;    }    let lisi = new Person('lisi');复制代码

在这个栗子中可以找到两条原型链,我们逐一来看。

  • 第一条:首先,lisi.__proto__ === Person.prototype,而原型对象也是对象,所以Person.prototype.__proto__ === Object.prototype,最后,Object.prototype.__proto__ === null。即:
lisi.__proto__.__proto__.__proto__ === null;复制代码
  • 第二条:Person这个函数对象的__proto__指向的应该是它的构造函数对应的原型对象,Person.__proto__ === Funcion.prototype,然后Funcion.prototype.__proto__ === Object.prototype,最后一样都回到null。即:
Person.__proto__.__proto__.__proto__ === null;复制代码

到这里,相信你已经可以理解文章开头的那张图了。

new方法做了什么

文章中创建实例对象是通过new 运算符。new命令的作用,就是执行构造函数,返回一个实例对象。

那么在执行new操作的过程中到底做了哪些事呢?我们可以看到,new 操作返回的实例对象具有两个特征:

  1. 具有构造函数中定义的this指针的属性和方法
  2. 具有构造函数原型上的属性和方法

于是我们大概可以知道,使用new 命令时它所执行的几个步骤:

  1. 创建一个空对象,并将这个空对象的__proto__,指向构造函数的原型对象[prototype],使其继承构造函数原型上的属性。
  2. 改变构造函数内部this指针为这个空对象(如果有传参,需要将参数也导入构造函数)
  3. 执行构造函数中的代码,使其具有构造函数this指针的属性。

所以我们可以简单模拟实现一个具有new命令功能的函数。

function newObj() {        let o, f = [].shift.call(arguments); // 取出参数的第一个成员,即构造函数        o = Object.create(f.prototype); // 创建一个继承了构造函数原型的新对象        f.call(o, ...arguments); // 执行构造函数使得新对象获取相应属性        return o;    }    let zs = newObj(Person, 'zs');    console.log(zs instanceof Person); // true复制代码

我们打印一下zs实例对象:

可以看出zs是继承了Person的原型的,但是还有一个需要注意的点:假如构造函数return了一个对象的话,new 命令会优先返回构造函数return的对象。如果是其他类型的数据,则会忽略,和没有返回值(函数默认返回undefined)是一样的。这里就不再举例,感兴趣的伙伴可以自己实践一下,也有助于理解。

总结

  1. 原型链,其中又包括prototypeconstructor__proto__这几个知识点,比如原型的继承、__proto__的原理、constructor的指向这些都是面试题中的熟面孔。
  2. new命令所做的几件事、new实例化的对象具有的特点以及模拟实现new命令的函数。

相关文章

由衷感谢这些文章的作者。

  • 《你不知道的JavaScript》

交流群(960807765)

前端交流群,欢迎各种技术交流,期待你的加入

后记

如果你看到了这里,且本文对你有一点帮助的话,希望你可以动动小手支持一下作者,感谢?。文中如有不对之处,也欢迎大家指出,共勉。

  • 文章代码库
  • 作者开源作品

转载地址:http://tajbx.baihongyu.com/

你可能感兴趣的文章
linux软件包管理之三(源代码安装)
查看>>
数据库三范式是什么?
查看>>
[转载]设置Ubuntu自动连接无线,无须再输入密钥环和无线密码
查看>>
九叔Xen App测试报告
查看>>
Apache配置
查看>>
Ext gridPanel 单元格数据的渲染
查看>>
Android SDK 的下载代理
查看>>
Method Swizzling对Method的要求
查看>>
佛祖保佑,永不宕机
查看>>
四、配置开机自动启动Nginx + PHP【LNMP安装 】
查看>>
LNMP一键安装
查看>>
SQL Server数据库概述
查看>>
Linux 目录结构及内容详解
查看>>
startx命令--Linux命令应用大词典729个命令解读
查看>>
华为3026c交换机配置tftp备份命令
查看>>
Oracle命令导入dmp文件
查看>>
OCP读书笔记(24) - 题库(ExamD)
查看>>
Http、TCP/IP协议与Socket之间的区别(转载)
查看>>
解决Unable to load R3 module ...VBoxDD.dll (VBoxDD):GetLastError=1790
查看>>
.net excel利用NPOI导入oracle
查看>>