目录

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的原型链。

十一、总结

这里例举几点比较重要的概念:

  1. 进行对象属性查找,首先会在当前对象查找,如果没有就会继续去查找内置[[prototype]]关联的对象,这个原型链会一直到Object.prototype,如果还是找不到就返回undefined。
  2. 构造函数只是函数,没有任何区别,使用new调用函数就是构造函数调用。
  3. JavaScript没有类,默认下不会复制,对象之间通过[[prototype]]进行关联,对象关联是原型中很重要的概念!

有问题就留言交流我很乐意

完全理解JS原型指南的更多相关文章

  1. 简单粗暴地理解js原型链–js面向对象编程

    简单粗暴地理解js原型链–js面向对象编程 作者:茄果 链接:http://www.cnblogs.com/qieguo/archive/2016/05/03/5451626.html 原型链理解起来 ...

  2. 【学习笔记】深入理解js原型和闭包系列学习笔记——精华

    深入理解js原型和闭包笔记: 1.“一切皆是对象”,对象是属性的集合. 丨 函数也是对象,但是使用typeof时为什么函数返回function而 丨  不是object呢,js为何要对函数做这样的区分 ...

  3. 【学习笔记】深入理解js原型和闭包(18)——补充:上下文环境和作用域的关系

    本系列用了大量的篇幅讲解了上下文环境和作用域,有些人反映这两个是一回儿事.本文就用一个小例子来说明一下,作用域和上下文环境绝对不是一回事儿. 再说明之前,咱们先用简单的语言来概括一下这两个的区别. 0 ...

  4. 【学习笔记】深入理解js原型和闭包(17)——补this

    本文对<深入理解js原型和闭包(10)——this>一篇进行补充,原文链接:https://www.cnblogs.com/lauzhishuai/p/10078307.html 原文中, ...

  5. 【学习笔记】深入理解js原型和闭包(16)——完结

    之前一共用15篇文章,把javascript的原型和闭包讲解了一下. 首先,javascript本来就“不容易学”.不是说它有多难,而是学习它的人,往往都是在学会了其他语言之后,又学javascrip ...

  6. 【学习笔记】深入理解js原型和闭包(15)——闭包

    前面提到的上下文环境和作用域的知识,除了了解这些知识之外,还是理解闭包的基础. 至于“闭包”这个词的概念的文字描述,确实不好解释,我看过很多遍,但是现在还是记不住. 但是你只需要知道应用的两种情况即可 ...

  7. 【学习笔记】深入理解js原型和闭包(14)——从【自由变量】到【作用域链】

    先解释一下什么是“自由变量”. 在A作用域中使用的变量x,却没有在A作用域中声明(即在其他作用域中声明的),对于A作用域来说,x就是一个自由变量.如下图 如上程序中,在调用fn()函数时,函数体中第6 ...

  8. 【学习笔记】深入理解js原型和闭包(13)——【作用域】和【上下文环境】

    上文简单介绍了作用域,本文把作用域和上下文环境结合起来说一下,会理解的更深一些. 如上图,我们在上文中已经介绍了,除了全局作用域之外,每个函数都会创建自己的作用域,作用域在函数定义时就已经确定了.而不 ...

  9. 【学习笔记】深入理解js原型和闭包(12)——简介【作用域】

    提到作用域,有一句话大家(有js开发经验者)可能比较熟悉:“javascript没有块级作用域”.所谓“块”,就是大括号“{}”中间的语句.例如if语句: 再比如for语句: 所以,我们在编写代码的时 ...

随机推荐

  1. Excel在线预览(通过poi转html,含里面的图片)

    支持03和07excel转html,直接上代码 测试类 /** * 主方法 * @author asus * */ public class App2 { public static void mai ...

  2. 【IntelliJ IDEA】 常用快捷键列表

    1.常用Shortcut F2 或Shift+F2 高亮错误或警告快速定位 Ctrl+Up/Down 光标跳转到第一行或最后一行下 Ctrl+B 快速打开光标处的类或方法  CTRL+ALT+B  找 ...

  3. SpringCloud番外篇-服务治理之Nacos

    一.Nacos概述 Nacos是阿里巴巴开源的服务注册中心,官方文档:https://nacos.io/zh-cn/docs/what-is-nacos.html 从个人使用体验上看,nacos要比e ...

  4. 安装实时查看日志工具 log.io

    官网:http://logio.org/ 一.环境 [root@centos ~]# cat /etc/system-release CentOS release 6.5 (Final) [root@ ...

  5. H5 + WebGL 实现的楼宇自控 3D 可视化监控

    前言 智慧楼宇和人们的生活息息相关,楼宇智能化程度的提高,会极大程度的改善人们的生活品质,在当前工业互联网大背景下受到很大关注.目前智慧楼宇可视化监控的主要优点包括: 智慧化 -- 智慧楼宇是一个生态 ...

  6. day4-字符串专区

    1.字符串 str (用''or“”表示) 字符串中每个组成部分为字符,python中只要是用引号引起来的都叫字符串 ---特征: 加法 n1 = "alex" n2 = &quo ...

  7. Linux软件包管理和磁盘管理实践

    一.自建yum仓库,分别为网络源和本地源 本地yum仓库的搭建就是以下三个步骤: 创建仓库目录结构 上传相应的包到目录下,或者直接挂载光盘也行,如果挂载光盘,第三步就可以省略,因为光盘默认里有repo ...

  8. MySQL InnoDB MVCC

    MySQL 原理篇 MySQL 索引机制 MySQL 体系结构及存储引擎 MySQL 语句执行过程详解 MySQL 执行计划详解 MySQL InnoDB 缓冲池 MySQL InnoDB 事务 My ...

  9. (C#)WPF:LinearGradientBrush的使用

    在MSDN文档库里可以查到,Rectangle.Fill的类型是Brush.Brush是一个抽象类,凡是以Brush为基类的类都可作为Fill属性的值.Brush的派生类有很多: * SolidCol ...

  10. 一个ip, 两个域名, 两个ssl, 对应多个不同的项目 之 坑

    之前配置了好几天, 就想通过tomcat直接配置. 找各种资料, 都说先配置Connector, 在配置Host. 我试了很多次, 都不成功. 原因我也没有找到在哪里. 我的配置参考如下网址: 修改这 ...