完全理解JS原型指南
目录
Table of Contents generated with DocToc
一、参考书籍和数据
翻看了几本JS书籍,其中主要有以下几本:《JavaScript高级程序设计第三版》、《你不知道的JavaScript卷一》、《JavaScript权威指南》以及查看了MDN文档。文章主要说了JavaScript中原型的一些概念知识,花了一点时间去总结,如任何问题的话可以提出来一起交流解决。文章中的图大多是从网络和书中截取下来,并非本人原创。
二、原型,[[prototype]]和.prototype以及constructor
结合书中的概念,原型是什么这个问题,可以这样去解释:原型就是一个引用(也就是指针),指向原型对象。这并不是废话,很多人说原型,实际上没意识到它只是一个引用,指向原型对象。原型在实例对象和构造函数中有不同的名称属性,但总是指向原型对象。如图所示:
[[prototype]]和.prototype以及constructor
- 其中的constructor是原型对象的属性,引用的是对象关联的函数,不可枚举,但这个属性是可以修改的,因此不可靠。
- 在实例对象中,原型就是对象的`[[prototype]]`内置属性(双方括号代表这是JavaScript引擎内部使用的属性/方法,正常JS代码无法访问,但可以通过`__proto__`访问到,后面会说到),在对象被创建时就包含了该属性,指向它的构造函数的原型对象。
- 在函数中,原型就是函数的`.prototype`属性,在函数被创建时就包含该属性,指向构造函数的原型对象 。
三、原型链
要理解原型链,首先需要明白原型对象的作用就是让所有实例对象共享它的属性和方法。根据上图,不难发现,person1和person2中的内部属性[[prototype]]都指向Person原型对象。当进行对象属性查找的时候,比如person1.name,首先会检查对象本身是否有这个属性,如果没有就继续去查找该对象[[prototype]]指向的原型对象中是否有该属性,如果还是没有就继续去找这个原型对象的[[prototype]]指向的原型对象(注意,原型对象也是有他自己的[[prototype]]属性的)!这个过程会持续找到匹配的属性名或查找完整的原型链
。不难理解了,原型链就是:每个实例对象( object )都有一个私有属性(称之为[[prototype]])指向它的构造函数的原型对象(prototype )。该原型对象也有一个自己的原型对象( [[prototype]] ) ,层层向上直到一个对象的原型对象为Object.prototype
(因为所有对象都是源于Object.prototype,其中包含许多通用的功能方法)。显然,如果找完这个原型链都找不到就会返回undefined。这个过程可以用一张图描述:
显然,原型和原型链的作用就是:如果对象上没有知道需要的属性和方法引用,JS引擎就会继续在[[prototype]]关联的对象上进行查找。这也是原型和原型链存在的意义。
for...in和in操作符
两个跟原型链有关的操作
- for...in遍历对象时,任何可以通过原型链访问到的(并且是enumerable为true)属性都会被枚举。
- in操作符用于检测属性在对象中是否存在,同样是会查找整条原型链。
function Person(name){
this.name = name;
}
Person.prototype.sayName = function() {
return this.name;
}
let myObject = new Person('练习生');
// 输出两个属性:name和sayName,其中sayName是原型对象中的属性
for(let key in myObject) {
console.log(key);
}
// 输出true,表示不可枚举的constructor存在于myObject中。
// 事实上constructor是在Person.prototype对象中
console.log("constructor" in myObject);
四、属性设置和屏蔽
给对象设置属性并不仅仅是添加一个属性或修改已有属性。这个过程应该是这样的:
// myObject的声明在第一个代码块
// 注意:sayName在Person.prototype中存在,将屏蔽原型链上的sayName方法
myObject.sayName = function() {
return `my name is:${this.name}`;
}
// 注意:age在myObject的整个原型链都不存在,将在实例中新建age属性
myObject.age = 23;
// 完成上述对myObject属性的设置,再新建一个对象
let myObject_1 = new Person('James');
// 查找myObject的属性和方法
myObject.age; //23
myObject.sayName(); // my name is: Bob
// 查找myObject_1的属性和方法
myObject.age; // undefined
myObject.sayName(); // 'Cat'
直接设置实例属性,都会屏蔽原型链上的所有同名属性(前提是属性的writable为 true,并且属性没有setter),并有以下两种情况:
- 当sayName属性不直接存在对象中而存在于原型链上层时,将会在myObjet中直接添加sayName属性,注意它只会阻止访问原型链上层的sayName属性,但不会修改按个属性。
- 当原型链上找不到age,则age直接添加到myObject中。
五、JavaScript只有对象
在面向对象语言中,类是可以被实例化多次,就像使用模具制作东西一样,对于每一个实例都会重复这个过程。但在JavaScript中,没有类,没有复制机制。只能创建多个对象,通过它们的内置[[prototype]]关联同一个原型对象。默认情况下,它们是关联的,并非复制,因为是同一个原型对象所以它们之间也不会完全失去联系。
比如说,new Person()生成一个对象,同时这个新对象的内置[[prototype]]关联的是Person.prototype对象。这里得到了两个对象,它们之间仅仅互相关联,并没有初始化类,如图所示:
这种机制也就是所谓的原型继承。这种Person()函数不算是类,它只是利用了函数的prototype属性“模仿类”而已!所以说,JavaScript没有类只有对象。
六、构造函数和new关键字
文章第一个代码块很容易让人认为Person是一个构造函数,因为使用new调用并看到他构造了一个对象。但其实Person跟其他普通函数没有什么不同,函数本身不是构造函数,所有的一切只是在函数调用前加了new
关键字!这样就会把这个函数调用变成一个“构造函数调用”。new会劫持所有普通函数并用构造对象的形式去调用它。下面这段代码可以证明这点:
function BaseFunction() {
console.log('Not a constructor!');
}
let myObject = new BaseFunction();
// Not a constructor.
typeof myObject; // object
BaseFunction是一个普通函数并非构造函数,但通过new调用,却会构造出一个对象。因此,构造函数其实是所有带new的函数调用。
七、模仿类
前面已经明确说过,JavaScript中只有对象,没有真正的类,但JavaScript开发者通过下面两种方法可以模拟类,如下代码所示:
function Foo(name) {
this.name = name;
}
Foo.prototype.myName = function() {
return this.name;
}
let a = new Foo('a');
let b = new Foo('b');
a.myName(); // a
b.myName(); // b
- this.name = name 给每一个new调用构造出来的对象都添加了.name属性(this绑定当前对象),这有点类似面向对象中“类实例封装的数据值”。
- Foo.prototype.myName = ...,给原型对象添加方法,那么通过该构造函数调用创建的实例就能共享原型对象的方法和属性。因此,a.myName和b.myName都可以正常工作,这有点类似面向对象中的什么?这点我还不知道,反正就是面向对象设计模式的一种。有知道的可以留言告诉我。
八、对constructor的错误理解
接上面的代码所示,如果继续运行a.constructor === Foo,返回的是true,因此有这种错误观点:对象由Foo构造。现在是时候把这个错误观点改过来了。constructor是存在于Foo.prototype中,a对象只是[[prototype]]委托找到constructor!这和构造毫无关系,下面代码可以证明这一点:
function Foo(){}
//将Foo的原型对象指向一个空对象
Foo.prototype = {};
let a = new Foo();
a.constructor === Foo; //false
a.constructor === Object; // true
嗯哼?现在你还敢说constructor表示a由Foo构建吗?按照这种错误观点,a.constructor === Foo应该返回true!其实constructor在只是创建函数时一个默认属性,指向prototype属性所在的函数。constructor属性时可以被修改的,让原型对象指向新的对象的时候,为了让constructor指向之前的函数,可以手动使用defineProperty方法添加一个不可枚举constructor属性。但真的很麻烦,总而言之不要太信任constructor属性!
九、原型继承
从这张图,可看出三点
- a1/a2到Foo.prototype,b1/b2到Bar.prototype的委托关联
- Bar.Prototype到Foo.prototype的委托关联
- 箭头由下到上表明这是委托关联而不是复制操作,否则如果是复制操作箭头应该回事由上往下。
下面这段代码是典型的原型继承风格
function Foo(name){
this.name = name;
}
Foo.prototype.myName = function() {
return this.name;
}
function Bar(name, label) {
Foo.call(this, name);
this.label = label;
}
// 将新的Bar原型对象和Foo的原型对象进行关联
Bar.prototype = Object.create(Foo.prototype);
Bar.prototype.myLabel = function() {
return this.label;
}
let a = new Bar("a", "obj a");
a.myName();
a.myLabel();
- 上面代码中,Bar.prototype = Object.create(Foo.prototype)表示创建新的Bar.prototype对象并关联到Foo.Prototype中。注意,这其实是把旧的Bar.prototype对象抛弃掉,再引用新的已关联到Foo.prototype的对象。
- ES6新增
Object.setPrototypeOf(obj1, obj2)
,表示直接将obj1的[[prototype]]关联到为obj2。
以下两行代码都是错误的对象关联做法:
Bar.prototype = Foo.prototype;
Bar.prototype = new Foo();
- 第一行代码只是让Bar的原型对象直接引用Foo的原型对象。如果对Bar.prototype的属性进行修改,则会影响到Foo.prototype本身。
- 第二行代码,在《JavaScript高级程序设计第三版》的示例代码出现。一开始觉得没问题,后来在《你不知道的JavaScript》中,它指出是错误的做法,原因是Foo函数如果会有一些副作用(比如给this添加数据就很不好),会影响到Bar()的实例。
十、类之间的关系
检查一个实例和祖先通常称为反射或内省。在JavaScript中通常用到
- 使用
a instanceof Foo
操作符,instanceof表示的是:在对象a的原型链上是否有指向Foo.prototype的对象。注意,instanceof的左侧是对象,右侧是函数。 - 使用
a.isPrototypeOf(b)
,isPrototypeOf表示的是:在对象a的整条原型链上是否出现过b。 - 使用
Object.getPrototypeOf(a)
,可以直接得到一个对象a的原型链。
十一、总结
这里例举几点比较重要的概念:
- 进行对象属性查找,首先会在当前对象查找,如果没有就会继续去查找内置[[prototype]]关联的对象,这个原型链会一直到Object.prototype,如果还是找不到就返回undefined。
- 构造函数只是函数,没有任何区别,使用new调用函数就是构造函数调用。
- JavaScript没有类,默认下不会复制,对象之间通过[[prototype]]进行关联,对象关联是原型中很重要的概念!
有问题就留言交流我很乐意
完全理解JS原型指南的更多相关文章
- 简单粗暴地理解js原型链–js面向对象编程
简单粗暴地理解js原型链–js面向对象编程 作者:茄果 链接:http://www.cnblogs.com/qieguo/archive/2016/05/03/5451626.html 原型链理解起来 ...
- 【学习笔记】深入理解js原型和闭包系列学习笔记——精华
深入理解js原型和闭包笔记: 1.“一切皆是对象”,对象是属性的集合. 丨 函数也是对象,但是使用typeof时为什么函数返回function而 丨 不是object呢,js为何要对函数做这样的区分 ...
- 【学习笔记】深入理解js原型和闭包(18)——补充:上下文环境和作用域的关系
本系列用了大量的篇幅讲解了上下文环境和作用域,有些人反映这两个是一回儿事.本文就用一个小例子来说明一下,作用域和上下文环境绝对不是一回事儿. 再说明之前,咱们先用简单的语言来概括一下这两个的区别. 0 ...
- 【学习笔记】深入理解js原型和闭包(17)——补this
本文对<深入理解js原型和闭包(10)——this>一篇进行补充,原文链接:https://www.cnblogs.com/lauzhishuai/p/10078307.html 原文中, ...
- 【学习笔记】深入理解js原型和闭包(16)——完结
之前一共用15篇文章,把javascript的原型和闭包讲解了一下. 首先,javascript本来就“不容易学”.不是说它有多难,而是学习它的人,往往都是在学会了其他语言之后,又学javascrip ...
- 【学习笔记】深入理解js原型和闭包(15)——闭包
前面提到的上下文环境和作用域的知识,除了了解这些知识之外,还是理解闭包的基础. 至于“闭包”这个词的概念的文字描述,确实不好解释,我看过很多遍,但是现在还是记不住. 但是你只需要知道应用的两种情况即可 ...
- 【学习笔记】深入理解js原型和闭包(14)——从【自由变量】到【作用域链】
先解释一下什么是“自由变量”. 在A作用域中使用的变量x,却没有在A作用域中声明(即在其他作用域中声明的),对于A作用域来说,x就是一个自由变量.如下图 如上程序中,在调用fn()函数时,函数体中第6 ...
- 【学习笔记】深入理解js原型和闭包(13)——【作用域】和【上下文环境】
上文简单介绍了作用域,本文把作用域和上下文环境结合起来说一下,会理解的更深一些. 如上图,我们在上文中已经介绍了,除了全局作用域之外,每个函数都会创建自己的作用域,作用域在函数定义时就已经确定了.而不 ...
- 【学习笔记】深入理解js原型和闭包(12)——简介【作用域】
提到作用域,有一句话大家(有js开发经验者)可能比较熟悉:“javascript没有块级作用域”.所谓“块”,就是大括号“{}”中间的语句.例如if语句: 再比如for语句: 所以,我们在编写代码的时 ...
随机推荐
- 20190723_C中使用API函数
学习关于API函数的格式 #include <stdlib.h> #include <string.h> #include <stdio.h> #pragma wa ...
- sql中实现先排序后分组
数据表结构和数据如下: CREATE TABLE `commun_message_chat_single` ( `id` ) NOT NULL AUTO_INCREMENT, `chat_id` ) ...
- CentOS 7升级Python到3.6.6后yum出错问题解决总结
最近将一台测试服务器操作系统升级到了Cent0S 7.5,然后顺便也将Python从2.7.5升级到Python 3.6.6,升级完成后,发现yum安装相关包时出现异常,报"File & ...
- windows下离线安装mysql8.0服务(支持多个安装,端口不同就可以)
1.官网下载 mysql文件.官网下载链接:https://dev.mysql.com/downloads/mysql/ 选择mysql下载的系统版本. 此处可以下载MSI安装包,图简单的朋友可以 ...
- python学习之【第十二篇】:Python中的迭代器
1.为何要有迭代器? 对于序列类型:字符串.列表.元组,我们可以使用索引的方式迭代取出其包含的元素.但对于字典.集合.文件等类型是没有索引的,若还想取出其内部包含的元素,则必须找出一种不依赖于索引的迭 ...
- linux常用的命令解释
1.man命令的操作按键: 按键 用处 空格键 向下翻一页. [Page Down] 向下翻一页. [Page Up] 向上翻一页. [HOME] 直接前往首页. [END] 直接前往尾页. /关键词 ...
- 1114作业 html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8&quo ...
- Springboot中的缓存Cache和CacheManager原理介绍
背景理解 什么是缓存,为什么要用缓存 程序运行中,在内存保持一定时间不变的数据就是缓存.简单到写一个Map,里面放着一些key,value数据,就已经是个缓存了 所以缓存并不是什么高大上的技术,只是个 ...
- linux内核崩溃之kdump机制
kdump相关概念 standard(production) kernel 生产内核 ,是指我们正在使用的kernel. Crash(capture)kernel 捕 ...
- 反射与泛型--使用泛型反射API打印出给定类的所有内容
package chapter8Demos; import java.lang.reflect.*; import java.util.Arrays; import java.util.Scanner ...