程序员人生 网站导航

你不知道的JavaScript--Item14 使用prototype的几点注意事项

栏目:htmlcss时间:2016-02-29 17:04:17

1、在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方法在每一个实例上都有1份拷贝。就像下图表示的那样:

这里写图片描述

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

这里写图片描述

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

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

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

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

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

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

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

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

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

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

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

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

1个类型的prototype和该类型的实例之间是”1对多“的关系。那末,需要确保实例相干的数据不会被毛病地保存在prototype之上。比如,对1个实现了树结构的类型而言,将它的子节点保存在该类型的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上时,所有实例的状态都会被集中地保存,在上面这类场景中明显是不正确的:本来属于每一个实例的状态被毛病地同享了。以下图所示:

这里写图片描述

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

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

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

这里写图片描述

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

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

4、避免继承标准类型

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

比如为了操作1个目录,可让目录类型继承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标准中,定义了1个不可见的内部属性被称为 [[class]]。该属性的值只是1个字符串,所以不要被误导认为JavaScript也实现了自己的类型系统。所以,对Array类型,这个属性的值就是“Array”;对Function类型,这个属性的值就是“Function”。下表是ECMAScript定义的所有[[class]] 值:

那末当对象的类型确切是Array时,length属性的特别的地方就在于:length的值会和该对象中被索引的属性个数保持1致。比如对1个数组对象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); };

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

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

------分隔线----------------------------
------分隔线----------------------------

最新技术推荐