跟我学习javascript的prototype使用注意事项


Posted in Javascript onNovember 17, 2015

一、在prototype上保存方法

不使用prototype进行JavaScript的编码是完全可行的,例如:

function User(name, passwordHash) { 
 this.name = name; 
 this.passwordHash = passwordHash; 
 this.toString = function() { 
  return "[User " + this.name + "]"; 
 }; 
 this.checkPassword = function(password) { 
  return hash(password) === this.passwordHash; 
 }; 
} 

var u1 = new User(/* ... */); 
var u2 = new User(/* ... */); 
var u3 = new User(/* ... */);

当创建了多个User类型的实例时,就存在问题了:不仅是name和passwordHash属性在每个实例上都存在,toString和checkPassword方法在每个实例上都有一份拷贝。就像下图表示的那样:

跟我学习javascript的prototype使用注意事项

但是,当toString和checkPassword被定义在prototype上时,上图就变成下面这个样子了:

跟我学习javascript的prototype使用注意事项

toString和checkPassword方法现在定义在了User.prototype对象上,也就意味着这两个方法只存在一份拷贝,并被所有的User实例共享。

也许你会认为将方法作为拷贝放在每个实例上,会节省方法查询的时间。(当方法定义在prototype上时,首先会在实例本身上寻找方法,如果没有找到才会去prototype上继续找)

但是在现代的JavaScript执行引擎中,对方法的查询进行了大量优化,所以这个查询时间几乎是不需要考虑的,那么将方法放在prototype对象上就节省了很多内存。

二、使用闭包来保存私有数据

JavaScript的对象系统从其语法上而言并不鼓励使用信息隐藏(Information Hiding)。因为当使用诸如this.name,this.passwordHash的时候,这些属性默认的访问级别就是public的,在任何位置都能够通过obj.name,obj.passwordHash来对这些属性进行访问。

在ES5环境中,也提供了一些方法来更方便的访问一个对象上所有的属性,比如Object.keys(),Object.getOwnPropertyNames()。所以,一些开发人员使用一些规约来定义JavaScript对象的私有属性,比如最典型的是使用下划线作为属性的前缀来告诉其他开发人员和用户这个属性是不应该被直接访问的。

但是这样做,并不能从根本上解决问题。其他开发人员和用户还是能够对带有下划线的属性进行直接访问。对于确实需要私有属性的场合,可以使用闭包进行实现。

从某种意义而言,在JavaScript中,闭包对于变量的访问策略和对象的访问策略是两个极端。闭包中的任何变量默认都是私有的,只有在函数内部才能访问这些变量。比如,可以将User类型实现如下:

function User(name, passwordHash) { 
 this.toString = function() { 
  return "[User " + name + "]"; 
 }; 
 this.checkPassword = function(password) { 
  return hash(password) === passwordHash; 
 }; 
}

此时,name和passwordHash都没有被保存为实例的属性,而是通过局部变量进行保存。然后根据闭包的访问规则,实例上的方法可以对它们进行访问,而在其它地方则不能。

使用这种模式的一个缺点是,利用了局部变量的方法都需要被定义在实例本身上,不能讲这些方法定义在prototype对象上。正如在Item34中讨论的那样,这样做的问题是会增加内存的消耗。但是在某些特别的场合下,即使将方法定义在实例上也是可行的。

三、实例状态只保存在实例对象上

一个类型的prototype和该类型的实例之间是”一对多“的关系。那么,需要确保实例相关的数据不会被错误地保存在prototype之上。比如,对于一个实现了树结构的类型而言,将它的子节点保存在该类型的prototype上就是不正确的:

function Tree(x) { 
 this.value = x; 
} 
Tree.prototype = { 
 children: [], // should be instance state! 
 addChild: function(x) { 
  this.children.push(x); 
 } 
}; 

var left = new Tree(2); 
left.addChild(1); 
left.addChild(3); 

var right = new Tree(6); 
right.addChild(5); 
right.addChild(7); 

var top = new Tree(4); 
top.addChild(left); 
top.addChild(right); 

top.children; // [1, 3, 5, 7, left, right]

当状态被保存到了prototype上时,所有实例的状态都会被集中地保存,在上面这种场景中显然是不正确的:本来属于每个实例的状态被错误地共享了。如下图所示:

跟我学习javascript的prototype使用注意事项

正确的实现应该是这样的:

function Tree(x) { 
 this.value = x; 
 this.children = []; // instance state 
} 
Tree.prototype = { 
 addChild: function(x) { 
  this.children.push(x); 
 } 
};

此时,实例状态的存储如下所示:

跟我学习javascript的prototype使用注意事项

可见,当本属于实例的状态被共享到prototype上时,也许会产生问题。在需要在prototype上保存状态属性前,一定要确保该属性是能够被共享的。

总体而言,当一个属性是不可变(无状态)的属性时,就能将它保存在prototype对象上(比如方法能够被保存在prototype对象上就是因为这一点)。当然,有状态的属性也能够被放在prototype对象上,这要取决于具体的应用场景,典型的比如用来记录一个类型实例数量的变量。使用Java语言作为类比的话,这类能够存储在prototype对象上的变量就是Java中的类变量(使用static关键字修饰)。

四、避免继承标准类型

ECMAScript标准库不大,但是提供了一些重要的类型如Array,Function和Date。在一些场合下,你也许会考虑继承其中的某个类型来实现特定的功能,但是这种做法并不被鼓励。

比如为了操作一个目录,可以让目录类型继承Array类型如下:

function Dir(path, entries) { 
 this.path = path; 
 for (var i = 0, n = entries.length; i < n; i++) { 
  this[i] = entries[i]; 
 } 
} 
Dir.prototype = Object.create(Array.prototype); 
// extends Array 

var dir = new Dir("/tmp/mysite", ["index.html", "script.js", "style.css"]); 
dir.length; // 0

但是可以发现,dir.length的值是0,而不是期待中的3。

发生这种现象的原因在于:只有当对象是真正的Array类型时,length属性才会起作用。

在ECMAScript标准中,定义了一个不可见的内部属性被称为 [[class]]。该属性的值只是一个字符串,所以不要被误导认为JavaScript也实现了自己的类型系统。所以,对于Array类型,这个属性的值就是“Array”;对于Function类型,这个属性的值就是“Function”。下表是ECMAScript定义的所有[[class]] 值:

那么当对象的类型确实是Array时,length属性的特别之处就在于:length的值会和该对象中被索引的属性个数保持一致。比如对于一个数组对象arr,arr[0]和arr[1]就表示该对象有两个被索引的属性,那么length的值就是2。当添加了arr[2]的时候,length的值会被自动同步成3。同样地,当设置length值为2时,arr[2]会被自动设置成undefined。

但是当继承Array类型并创建实例时,该实例的 [[class]] 属性并不是Array,而是Object。因此length属性不能正确的工作。

在JavaScript中,也提供了用于查询 [[class]] 属性的方法,即使用Object.prototype.toString方法:

var dir = new Dir("/", []); 
Object.prototype.toString.call(dir); // "[object Object]" 
Object.prototype.toString.call([]); // "[object Array]"

因此,更好的实现方法是使用组合而不是继承:

function Dir(path, entries) { 
 this.path = path; 
 this.entries = entries; // array property 
} 
Dir.prototype.forEach = function(f, thisArg) { 
 if (typeof thisArg === "undefined") { 
  thisArg = this; 
 } 
 this.entries.forEach(f, thisArg); 
};

以上代码将不再使用继承,而是将一部分功能代理给内部的entries属性来实现,该属性的值是一个Array类型对象。

ECMAScript标准库中,大部分的构造函数都会依赖内部属性值如 [[class]] 来实现正确的行为。对于继承这些标准类型的子类型,无法保证它们的行为是正确的。因此,不要继承ECMAScript标准库中的类型如:
Array, Boolean, Date, Function, Number,RegExp,String

以上就是对使用prototype的几点注意事项进行总结,希望可以帮助大家正确的使用prototype。

Javascript 相关文章推荐
走出JavaScript初学困境—js初学
Dec 29 Javascript
jquery获取css中的选择器(实例讲解)
Dec 02 Javascript
JavaScript实现从数组中选出和等于固定值的n个数
Sep 03 Javascript
Javascript中setTimeOut和setInterval的定时器用法
Jun 12 Javascript
jquery控制显示服务器生成的图片流
Aug 04 Javascript
AngularJs directive详解及示例代码
Sep 01 Javascript
简单的jQuery拖拽排序效果的实现(增强动态)
Feb 09 Javascript
Vue.js进行查询操作的实例详解
Aug 25 Javascript
详解使用Vue Router导航钩子与Vuex来实现后退状态保存
Sep 11 Javascript
jquery根据name取得select选中的值实例(超简单)
Jan 25 jQuery
VUE组件中的 Drawer 抽屉实现代码
Aug 06 Javascript
微信小程序实现多行文字滚动
Nov 18 Javascript
js弹出对话框方式小结
Nov 17 #Javascript
跟我学习javascript的prototype,getPrototypeOf和__proto__
Nov 17 #Javascript
Jquery 垂直多级手风琴菜单附源码下载
Nov 17 #Javascript
JavaScript代码实现禁止右键、禁选择、禁粘贴、禁shift、禁ctrl、禁alt
Nov 17 #Javascript
跟我学习javascript的undefined与null
Nov 17 #Javascript
跟我学习javascript的arguments对象
Nov 16 #Javascript
JavaScript函数学习总结以及相关的编程习惯指南
Nov 16 #Javascript
You might like
利用static实现表格的颜色隔行显示
2006/10/09 PHP
微信公众号之主动给用户发送消息功能
2019/06/22 PHP
JS子父窗口互相操作取值赋值的方法介绍
2013/05/11 Javascript
使用jquery实现简单的ajax
2013/07/08 Javascript
js判断空对象的实例(超简单)
2016/07/26 Javascript
jQuery双向列表选择器DIV模拟版
2016/11/01 Javascript
vue系列之动态路由详解【原创】
2017/09/10 Javascript
angular4实现tab栏切换的方法示例
2017/10/21 Javascript
详解Angular操作cookies方法
2018/06/01 Javascript
JavaScript中var的重要性实例分析
2019/07/09 Javascript
javascript实现简易聊天室
2019/07/12 Javascript
Vue.js计算机属性computed和methods方法详解
2019/10/12 Javascript
详解element上传组件before-remove钩子问题解决
2020/04/08 Javascript
Python中的条件判断语句基础学习教程
2016/02/07 Python
Python实现求两个csv文件交集的方法
2017/09/06 Python
Django自定义用户认证示例详解
2018/03/14 Python
浅析PHP与Python进行数据交互
2018/05/15 Python
python 获取键盘输入,同时有超时的功能示例
2018/11/13 Python
Windows 安装 Anaconda3+PyCharm的方法步骤
2019/06/13 Python
Python中判断子串存在的性能比较及分析总结
2019/06/23 Python
Python 使用type来定义类的实现
2019/11/19 Python
opencv 实现特定颜色线条提取与定位操作
2020/06/02 Python
HTML5 progress和meter控件_动力节点Java学院整理
2017/07/06 HTML / CSS
欧洲高端品牌直销店:Fashionesta
2016/08/31 全球购物
生产部主管岗位职责
2014/01/06 职场文书
护士自我介绍信
2014/01/13 职场文书
十佳班主任事迹材料
2014/01/18 职场文书
董事长助理工作职责
2014/06/08 职场文书
动画设计系毕业生求职信
2014/07/15 职场文书
国家奖学金获奖感言
2014/08/16 职场文书
滞留工资返还协议书
2014/10/19 职场文书
中学生综合素质自我评价
2015/03/06 职场文书
2015年会计工作总结范文
2015/05/26 职场文书
年会主持人开场白台词
2015/05/29 职场文书
2016年“抗战胜利纪念日”71周年校园广播稿
2015/12/18 职场文书
vue router 动态路由清除方式
2022/05/25 Vue.js