原来JS是这样的 - 原型链
上一篇提到属性描述符 [[Get]] 和 [[Put]] 以及提到了访问描述符 [[Prototype]],看它们的特性就会很容易的让人想到经典的面向对象风格体系中对类操作要做的事情,但带一些 introspector 的味道。回想到之前所写来自用的辣鸡私有云音乐应用中所附带了一个简易的类似 jQuery 的简易常用功能实现,就用到了简单的 [[Prototype]] 特性。但我们前几篇都没有详细的提及 js 的原型链相关的内容,本篇就将讨论 js 的 [[Prototype]] 属性和相关的内容。
注:ES6 的 Proxy 和 class 的概念不在本篇讨论范围内。
[[Prototype]]
JavaScript 中的特殊对象属性除了 [[Get]] 和 [[Put]] 外,还有一个很重要的特殊内置属性就是 [[Prototype]] 了。
[[Prototype]] 是一个几乎所有对象在创建时都会被赋予一个非空值的属性,还记得在之前提到 new 操作符的行为吗?其中的行为之一就是把其 [[Prototype]] 关联指向到对应的内置对象上。通常 [[Prototype]] 所指向的即为创建此对象时所使用的对象了。
来看下面一个例子
var macat = { a: 1 };
var codingcat = macat; // 和 macat 指向的内容相同
codingcat.b = 2;
console.log(macat.b); // 2
var pineapple = Object.create( macat ); // 新对象,但其 [[Prototype]] 链向 macat
pineapple.c = 3; // 新对象的属性
console.log(macat.c); // undefined
codingcat.d = 4;
console.log(pineapple.d) // 4;
上例中, 变量 codingcat 显然是指向和 macat 相同的内容,实质完全一致,而 pineapple 则是通过 Object.create() 创建的变量。显然 pineapple 和 macat 是不同的两个对象。不过我们会发现我们依然可以通过 pineapple.d 访问 macat.d 的值,这就是因为在 Object.create() 中,会把 pineapple 的 [[Prototype]] 指向我们的原型对象 macat 了。
那 [[Prototype]] 引用的作用是什么呢?看上去这是一个确定这种像 fallback 一样的取值操作应该 fallback 到谁的属性标记,而准确的说,这种 pineapple.d 形式的属性引用会触发 [[Get]] 操作(上篇的内容),而默认的 [[Get]] 则会在对象本身没有此属性时会去查找 [[Prototype]] 引用的变量了。这样的引用成为了链状,故被称作原型链。
当然,这个行为其实我们已经“用过”很多次了,比如 .toString()、 .valueOf()、hasOwnProperty(),我们 Object.create() 等形式构建的新对象显然并没有附带一份这些函数的副本,而是因为普通的 [[Prototype]] 链最终都会指向内置的 Object.prototype,而它提供了这些功能。
属性设置和屏蔽
不过上例中有个有趣的坑,我们考虑在上例的基础上做如下操作:
...
pineapple.a++; // 交互式终端会输出 1
console.log(pineapple.a); // 2
console.log(macat.a); // 1
pineapple.a++ 看上去是进行了变量自增的操作,但这一行后,我们发现 pineapple.a 不再等于 macat.a 了,这是因为实际上 pineapple.a 本来并不存在,但可以通过原型链找到 macat.a,而 pineapple.a++ (相当于 pineapple.a = pineapple.a + 1)最终进行的赋值操作创建了 pineapple.a ,故最终这两个变量的值自然不再相等。
这个例子来看,如果本身即通过对 pineapple 的属性(a)进行访问操作,那么不同情况下访问得到的结果可能是不同的甚至是出人意料的。无意中创建的属性“阻止”了原型链上查找这个属性的行为,我们称之为属性屏蔽。
属性屏蔽根据变量本身情况的不同会有很多不同的状态表现,例如原型链上层变量的数据访问属性标记为只读的情况,(如果不是严格模式下)尝试进行的赋值操作会被忽略等。
类 (迫真)
我们早已知道 JavaScript 中不存在“类”的概念,而为了能够“写着爽”,很多开发者都在想尽办法在 JavaScript 中模仿其它 OO 语言中“类”的行为。其中很常见的做法类似下面这样:
function Person(name) {
console.log("I'm " + name + "!");
this.name = name;
}
Person.prototype.getName = function() {
return this.name;
}
var chris = new Person("Chris"); // I'm Chris
var sophie = new Person("Sophie"); // I'm Sophie
chris.getName(); // "Chris"
看上去我们的 Person 像极了一个包含 name 成员变量和 getName() 方法的类,并且在其“构造函数”中会输出 "I'm xxx"。不过在之前的文章中我们已经讲过了,并不存在所谓的构造函数,new 只是把 Person() 函数作为构造对象所需调用的函数进行了一次调用而已。不过你可能还会比较奇怪为什么 .getName() 是可以使用的,既然我们在原型链这一章提起这件事,显然是因为原型链,于是回顾之前第二章我们含糊提到的一句话是(new 操作符所执行的操作步骤之一是)“对这个新对象执行 [[Prototype]] 链接”,实际上,这里我们被 new 出来的对象的 [[Prototype]] 被关联到了 Person.prototype 上,于是当我们尝试进行属性访问的时候,自然就可以访问到 Person.prototype.getName() 上了。
不过这个过程还是可能会引起一些蛋疼的误会,比如假设我们在上面例子的基础上:
...
sophie.constructor === Person; // true
sophie.constructor === Person.prototype.constructor; // true
Person.prototype = {};
var koishi = new Person("Koishi"); // I'm Koishi
koishi.constructor === Person; // false
koishi.constructor === Object; // true
sophie.constructor === Person; // true
sophie.constructor === Person.prototype.constructor; // false
由于“构造函数”这种表现形式的理解,我们有时候会认为 变量名.constructor 实际就总是构造调用时指向的函数,甚至 sophie.constructor === Person 返回也是 true ,但实际并不是这样,这里返回为真,仅仅是因为 Person.prototype.constructor 默认指向的就是 Person 罢了。于是我们尝试替换 Person.prototype 之后创建了变量 koishi,再检查 koishi.constructor === Person 就不再为真了,在原型链的查找过程最终找到了 Object.prototype,然后 Object.prototype.constructor 其实指向了 Object。
不过,后面我们接着尝试检查了 sophie.constructor 却发现似乎它并未受到影响,这个就不要往原型链方面想了,这里的原因仅仅是 sophie 的原型链指向的是曾经 Person.prototype 所指向的东西上,而我们 Person.prototype = {} 的操作只是让 Person.prototype 指向了新的东西,旧的东西并没有改变,所以 sophie 自然看上去“没有受到影响”了。当然,koishi 这个变量被构造时所被调用的函数仍然是 Person(),这和 koishi.constructor 或者 Person.prototype.constructor 的指向没有什么关系。
对象实例关系
当然我们还有一点需要重新强调的是,[[Prototype]] 和 .prototype 不是一回事,[[Prototype]] 是描述对象实例关系的属性描述符,而 .prototype 只是 Function 对象的一个属性而已。new 操作符会把新建的对象的 [[Prototype]] 指向原对象的 .prototype 属性上,仅此而已。
既然 [[Prototype]] 实际描述了对象之间的实例关系,那么我们自然就可以想到 instanceof 的实际作用了,其所做的事情就是告诉你在 a instanceof Foo 中, a 的整个原型链中是否有指向 Foo.prototype 的对象。
绝大多数浏览器支持一个 .__proto__ 属性(实际位于 Object.__proto__)指向了 [[Prototype]] ,这对于我们调试时希望直接访问内部的 [[Prototype]] 提供了便利,不过它并不是标准,所以除了调试便利之外还是不要使用它比较好。
最后
于是关于原型链相关的简单讨论就到此结束了。和上篇一样,如果你对这些内容仍然感兴趣,不妨去读一读《You don’t know JS - this & object prototypes》一书。这是一本开源书,你可以在这里在线阅读这本书,或者购买这本书的电子版或实体版。这本书的中文译本涵盖在《你所不知道的 JavaScript 上卷》中,你也可以考虑看中文版。
由于近期工作过于繁忙的精力占用缘故,“原来JS是这样的”系列可能就暂时告一段落了。最后,尽管我会尽可能仔细的检查文章内容是否有问题,但也不保证这篇文章中一定不会有错误,如果您发现文章哪里有问题,请在下面留言指正,或通过任何你找得到的方式联系我指正。感激不尽~
原来JS是这样的 - 原型链的更多相关文章
- 第20篇 js高级知识---深入原型链
前面把js作用域和词法分析都说了下,今天把原型链说下,写这个文章费了点时间,因为这个东西有点抽象,想用语言表达出来不是很容易,我想写的文章不是简单的是官方的API的copy,而是对自己的知识探索和总结 ...
- JS prototype chaining(原型链)整理中······
初学原型链整理 构造器(constructor).原型(prototype).实例(instance); 每一个构造器都有一个prototype对象,这个prototype对象有一个指针指向该构造器: ...
- JS对象继承与原型链
1.以复制方式实现的继承 1.1浅拷贝 基本类型的复制 var parent = { lanage: "chinese" } var child = { name: "x ...
- 面试题常考&必考之--js中的难点!!!原型链,原型(__proto__),原型对象(prototype)结合例子更易懂
1>首先,我们先将函数对象认识清楚: 补充snow的另一种写法: var snow =function(){}; 2>其次:就是原型对象 每当我们定义一个函数对象的时候,这个对象中就会包含 ...
- JS 面向对象之继承 -- 原型链
ECMAScript只支持实现继承,其实现继承主要是靠原型链来实现. 原型链的基本思想是利用原型让一个引用类型继承另一个引用类型的属性和方法. 简单回顾下构造函数.原型和实例的关系: 每个构造函数都有 ...
- 关于JS面向对象中原型和原型链以及他们之间的关系及this的详解
一:原型和原型对象: 1.函数的原型prototype:函数才有prototype,prototype是一个对象,指向了当前构造函数的引用地址. 2.函数的原型对象__proto__:所有对象都有__ ...
- 个人对JS原型链的一些理解(prototype、__proto__)
前言 在我一开始学习java web的时候,对JS就一直抱着一种只是简单用用的心态,于是并没有一步一步地去学习,当时认为用法与java类似,但是在实际web项目中使用时却比较麻烦,便直接粗略了解后开始 ...
- 前端【JS】,深入理解原型和原型链
对于原型和原型链,相信有很多伙伴都说的上来一些,但有具体讲不清楚.但面试的时候又经常会碰到面试官的死亡的追问,我们慢慢来梳理这方面的知识! 要理解原型和原型链的关系,我们首先需要了解几个概念:1.什么 ...
- 用ECMAScript4 ( ActionScript3) 实现Unity的热更新 -- 使用原型链和EventTrigger
原型链是JS的必备,作为ECMAScript4,原型链也是支持的. 特别说明,ActionScript3是支持完整的面向对象继承支持的,原型链只在某些非常特殊的情况下使用. 本文旨在介绍如何使用原型链 ...
随机推荐
- Python实现电子邮件的发送
利用Python smtplib.SMTP类方法来实现电子邮件的发送. 列举SMTP对象常见的方法: sendmail(from, to ,msg[,mopts,ropts]) :将msg从from发 ...
- Linux防火墙常用操作
/tcp —— 配置白名单 sudo systemctl start firewalld — 启动防火墙 sudo firewall-cmd --state - 看状态 sudo firewall-c ...
- Web前端助手-功能丰富的Chrome插件
整合优秀的前端实用工具.免费,可配置的强大工具集 示例 安装 github仓库: https://github.com/zxlie/FeHelper 官网地址:https://www.baidufe. ...
- Powershell寻找域管在线服务器
记录线下Powershell在域环境中对于服务器的信息收集 Powershell的脚本有很多,在内网渗透测试中不仅能扫,能爆,能转发,还能做更多的事情.我们常用的脚本有Powersploit,Empi ...
- [POI2015]PIE
题目描述 一张n*m的方格纸,有些格子需要印成黑色,剩下的格子需要保留白色.你有一个a*b的印章,有些格子是凸起(会沾上墨水)的.你需要判断能否用这个印章印出纸上的图案.印的过程中需要满足以下要求:( ...
- [JZOJ5775]【NOIP2008模拟】农夫约的假期
Description 在某国有一个叫农夫约的人,他养了很多羊,其中有两头名叫mm和hh,他们的歌声十分好听,被当地人称为“魔音”······ 农夫约也有自己的假期呀!他要去海边度假,然而mm和 ...
- Maven安装和在IDEA配置Maven
一.Windows安装Maven 1.下载Maven 这里需要注意:不要去官网下载最新的版本,因为会出现与IDEA不兼容的现象. 这里提供下载地址:https://archive.apache.org ...
- comparator接口实现时,只需要实现 int compare(T o1, T o2)方法?
从Comparator接口的源码,可以看到Comparator接口中的方法有三类: 1 普通接口方法 2 default方法 3 static方法 其中default方法和static方法 是java ...
- vue,element列表大数据卡顿问题,vue列表渲染慢,element表格渲染慢,表格渲染慢(卡),表格全选卡
https://github.com/livelyPeng/pl-table 一个表格组件(完美解决万级数据渲染卡顿问题),流畅渲染万级数据并不会影响到el-table的原有功能 分析: 前端UI框架 ...
- 记一次 XxlRpcException:xxl-rpc request timeout at 超时问题
事件起因 昨天有同事找我到,说他搭建的 XXL-JOB 任务调度系统不能工作了,调用总是出错(服务端返回 500)希望我能帮忙处理一下,不过说实话我也没有搭建过 XXL-JOB 的经验,但是既然同事请 ...