跟我学习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 相关文章推荐
prototype.js的Ajax对象
Sep 23 Javascript
浅谈tudou土豆网首页图片延迟加载的效果
Jun 23 Javascript
公共js在页面底部加载的注意事项介绍
Jul 18 Javascript
javascript修改IMG标签的src问题
Mar 28 Javascript
使用JavaScript和C#中获得referer
Nov 14 Javascript
jquery获取url参数及url加参数的方法
Oct 26 Javascript
打造自己的jQuery插件入门教程
Sep 23 Javascript
Angular2库初探
Mar 01 Javascript
jQuery判断邮箱格式对错实例代码讲解
Apr 12 jQuery
express.js中间件说明详解
Mar 19 Javascript
VUE前后端学习tab写法实例
Aug 06 Javascript
vue实现计算器功能
Feb 22 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
AMFPHP php远程调用(RPC, Remote Procedure Call)工具 快速入门教程
2010/05/10 PHP
PHP--用万网的接口实现域名查询功能
2012/12/13 PHP
与文件上传有关的php配置参数总结
2013/06/14 PHP
PHP中实现接收多个name相同但Value不相同表单数据实例
2015/02/03 PHP
PHP判断JSON对象是否存在的方法(推荐)
2016/07/06 PHP
PHP7.1新功能之Nullable Type用法分析
2016/09/26 PHP
php中简单的对称加密算法实现
2017/01/05 PHP
PHP输出多个元素的排列或组合的方法
2017/03/14 PHP
PHP设计模式之注册树模式分析
2018/01/26 PHP
php微信开发之关键词回复功能
2018/06/13 PHP
简单实例处理url特殊符号&amp;处理(2种方法)
2013/04/02 Javascript
JQuery 控制内容长度超出规定长度显示省略号
2014/05/23 Javascript
jquery修改网页背景颜色通过css方法实现
2014/06/06 Javascript
Javascript中innerHTML用法实例分析
2015/01/12 Javascript
JavaScript获取元素尺寸和大小操作总结
2015/02/27 Javascript
详解AngularJS如何实现跨域请求
2016/08/22 Javascript
微信小程序 es6-promise.js封装请求与处理异步进程
2017/06/12 Javascript
Vue中使用Lodop插件实现打印功能的简单方法
2019/12/19 Javascript
javascript中的offsetWidth、clientWidth、innerWidth及相关属性方法
2020/05/14 Javascript
python将数据插入数据库的代码分享
2020/08/16 Python
python通过函数名调用函数的几种场景
2020/09/23 Python
python 如何设置守护进程
2020/10/29 Python
python 检测nginx服务邮件报警的脚本
2020/12/31 Python
Python绘制K线图之可视化神器pyecharts的使用
2021/03/02 Python
前端隐藏出边界内容的实现方法
2016/04/14 HTML / CSS
CSS3模拟动画下拉菜单效果
2017/04/12 HTML / CSS
CSS3 毛玻璃效果
2019/08/14 HTML / CSS
HTML5 Canvas之测试浏览器是否支持Canvas的方法
2015/01/01 HTML / CSS
台湾旅游网站:雄狮旅游网
2017/08/16 全球购物
美国第二大连锁书店:Books-A-Million
2017/12/28 全球购物
24岁生日感言
2014/01/13 职场文书
网上开店必备创业计划书
2014/01/26 职场文书
主题婚礼策划方案
2014/02/10 职场文书
保护环境倡议书范文
2014/05/13 职场文书
开工仪式策划方案
2014/05/23 职场文书
创业计划书之DIY自助厨房
2019/09/06 职场文书