前言
最近看 JS 小红书, 特别是原型与原型链的概念, 蛮有收获感, 然后就想自己能不能总结下(顺便练练自己的写作水平 = =), 如果能的话, 说明自己掌握了, 于是就有这篇文章的~
准备进阶或者正在找工作的伙伴不妨也看看, 毕竟面试题会涉及到这个概念, 对不对? 虽然还在疫情的状况中, 但总要准备肯定没错的, 对吧? 如有错误的地方, 欢迎指出, 鄙人将感激不尽~
image
正文
原型
要理解原型的概念, 首先要理解 es 的面向对象, 不管是 ECMAScript, 还是 Java 或者 C 语言, 它们都具有面向对象的概念, 但面向对象有一个标志, 就是类的概念, 而 es 中却没有, 虽说 es6 已经推出类的概念, 其实上是语法糖而已, 为了更好地混上编程的江湖, 嘿嘿~
那么 es 靠什么实现面向对象呢? 是函数, 对, 就是这么一股清流的. 哎呀, 光说说有啥用, 作为一枚码农, 还是要结合概念和动手写代码才能懂, 对不对?
es 实现面向对象的代码是这样:
- function Person(name,age){
- this.name = name;
- this.age = age;
- this.sayName = function(){
- console.log(this.name);
- }
- }
- let person1 = new Person("Fernweh",22);
- person1.sayName();//"Fernweh"
- let person2 = new Person("Nancy",26);
- person2.sayName();//"Nancy"
首先创建一个函数, 其实是叫构造函数, 封装一个对象的属性以及方法. 然后想要调用这个函数, 只要通过 new 来调用, 就能共享这个函数的属性和方法.
这就是 es 的面向对象用法. 这看起来大法好, 但我不得不遗憾地宣告这个还是有缺点的. 伙伴们应该知道, 作为优秀的程序员, 首先肯定要考虑到内存的问题, 而上面的代码是会浪费内存, 为什么呢?
因为通过 new 来调用, 相当于拷贝到新开辟的内存, 如下图, 就是说多个 new, 就会生成多个内存, 这就很不友好了. 所以这就需要用到原型的概念~
image
原型是什么, 它又是怎么实现?
伙伴们平时写代码或者看别人的代码, 经常会看到 prototype 这个吧, 这个就是原型. 凡是创建的每个函数都有一个原型, 原型的属性是一个指针, 指向一个对象
- function Person(){
- }
- Person.prototype.name="Fernweh";
- Person.prototype.age=26;
- Person.prototype.sayName=function(){
- console.log(this.name);
- }
- let person1=new Person();
- let person2=new Person();
Person 是被创建的函数, 然后它有一个原型, 所以就把 name,age 和 sayName 方法放到这个函数的原型, 变成一个原型对象. WC, 怎么还有原型对象的概念(内心: 怎么越写越乱呢? 翻车了么? emmm), 别慌, 我用画图来说明~
image
就是这样, 看看这个, 就仅仅通过指针指向原型对象, 就能共享原型对象的 name,age,sayName 方法, 不需要开辟新的内存. 然而这个 [[prototype]] 这个啥意思? 稍后说下哈~
然而如果我想要改变 person1 的 name 呢? 就增加一行代码就好:
- person1.name="I am name";
- person1.sayName(); //I am name
image
现在明白了原型的作用吧~
image
这样很 ok 吧? 并不, 既然讲到原型, 那就讲到底, 因为这个还有点坑...JS 的世界就是那么奇妙的, 继续让我们来康康是什么样的坑~
image
在上面的代码, 我们增加一行代码, 数组类型:
- Person.prototype.args=[1,2,3,4];
- person1.args.push(5);
- console.log(person1.args);//[1,2,3,4,5]
- console.log(person2.args);//[1,2,3,4,5]
- console.log(Person.prototype.args);//[1,2,3,4,5]
有没有发现, 只是想给 person1 增加一个数组元素, 结果 person2 的 args 和原型对象的 args 都被改变了, 因为是引用的类型, 指向同一个内存地址, 所以才会同时被改变, 这就尴尬了, 既然实例化多个对象, 都应该是独立的实例对象.
image
所以小红书推荐组合使用构造函数和原型模式的方法, 就是一开头讲到的代码和上面原型的代码组合, 看下面的代码:
- function Person(name,age){
- this.name=name;
- this.age=age;
- this.args=[1,2,3,4];
- }
- Person.prototype.sayName=function(){
- console.log(this.name);
- }
- let person1=new Person("fernweh",12);
- let person2=new Person("nancy",12);
- person1.args.push(5);
- person1.args; //[1,2,3,4,5]
- person2.args; //[1,2,3,4]
发现没, 把属性放到构造函数里面去, 就不会被影响, 因为各自开辟新的内存, 所以就不是指向同一个内存地址. 所以最好的方法就是把属性放到构造函数里, 方法放到原型模式里去.
[[prototype]]
image
刚才说到的[[prototype]], 实际上就是我们常见的 proto, 表示一个指针, 指向原型对象, 如上图指向 Person.prototype, 下面我们可以用代码来验证
person1.__proto__===Person.prototype; //true
每次生成新实例, 就会有 [[prototype]] 的属性
伙伴们应该看过 new 的源码:
- function myNew(Con,...args){
- let obj={};
- obj.__proto__=Con.prototype;
- let res=Con.apply(obj,args);
- return res instanceof Object ? res : obj;
- }
其中 obj.__proto__=Con.prototype, 就是和上面的概念有关的.
原型链
原型链是一种实现继承的方法, 引用小红书的话:
其基本思想是利用原型让一个引用类型继承另一个引用类型的属性和方法. 简单回顾一下构造函数, 原型和实例的关系: 每个构造函数都有一个原型对象, 原型对象都包含一个指向构造函数的指针, 而实例都包含一个指向原型对象的内部指针. 那么, 假如我们让原型对象等于另一个类型的实例, 结果会怎么样呢? 显然, 此时的原型对象将包含一个指向另一个原型的指针, 相应地, 另一个原型中也包含着一个指向另一个构造函数的指针. 假如另一个原型又是另一个类型的实例, 那么上述关系依然成立, 如此层层递进, 就构成了实例与原型的链条, 这就是所谓原型链的基本概念.
啪, 这么长的引用, 而且太绕了吧~我好像看到星星的~
image
别急, 让我们来康康利用原型让一个引用类型继承另一个引用类型这句重点的话, 就是说有一个引用类型, 比如上面的 Perosn 构造函数, 然后新建另一个引用类型 function children(){}, 然后利用原型继承原来的引用类型, 就是说 children.prototype=new Person(), 就生成下面的图:
image
说白, 就是利用原型来实现的链条, 这就叫原型链, 好处就是能重用, 比如说我自己没有某个东西, 可以去上面寻找需要的东西, 如果有就直接返回给需要的东西.
好了, 就这样啦~
来源: http://www.jianshu.com/p/98fa0b3a7022