参考书籍:《Effective JavaScript》css
原型包括三个独立但相关的访问器。html
C.prototype
用于创建由new C()
建立的对象的原型。Object.getPrototypeOf(obj)
是ES5中用来获取obj对象的原型对象的标准方法。obj.__proto__
是获取obj对象的原型对象的非标准方法。function User(name, passwordHash) { this.name = name; this.passwordHash = passwordHash; } User.prototype.toString = function () { return '[User ' + this.name + ']'; }; User.prototype.checkPassword = function (password) { return hash(password) === this.passwordHash; } var u = new User('sfalken', '0ef33ae791068ec64b502d6cb0191387');
User函数带有一个默认的prototype属性,其包含一个开始几乎为空的对象。当咱们使用new操做符建立User的实例时,产生的对象u获得了自动分配的原型对象,该原型对象被存储在User.prototype
中。程序员
Object.getPrototypeOf(u) === User.prototype; // true
u.__proto__ === User.prototype; // true
提示:编程
C.prototype
属性是new C()
建立的对象的原型。Object.getPrototypeOf(obj)
是ES5中检索对象原型的标准函数。Obj.__proto__
是检索对象原型的非标准函数。__proto__属性提供了Object.getPrototypeOf
方法所不具有的额外能力,即修改对象原型连接的能力。这种能力会形成严重的影响,应当避免使用,缘由以下:设计模式
可使用ES5中的Object.create
函数来建立一个具备自定义原型链的新对象。数组
提示::安全
Object.create
函数给新对象设置自定义的原型。function User(name, passwordHash) { this.name = name; this.passwordHash = passwordHash; } var u = User('baravelli', 'd8b74df393528d51cd19980ae0aa028e'); u; // undefined this.name; // baravelli this.passwordHash; // d8b74df393528d51cd19980ae0aa028e
若是调用者忘记使用new关键字,该函数不但会返回无心义的undefined,并且会建立(若是这些全局变量已经存在则会修改)全局变量name和passwordHash。闭包
若是将User函数定义为ES5的严格代码,那么它的接收者默认为undefined。app
function User(name, passwordHash) { "use strict"; this.name = name; this.passwordHash = passwordHash; } var u = User('baravelli', 'd8b74df393528d51cd19980ae0aa028e'); // Uncaught TypeError: Cannot set property 'name' of undefined
一个更为健壮的方式是提供一个无论怎么调用都工做如构造函数的函数。函数
function User(name, passwordHash) { if (!this instanceof User) { return new User(name, passwordHash); } this.name = name; this.passwordHash = passwordHash; } var x = User('baravelli', 'd8b74df393528d51cd19980ae0aa028e'); var y = new User('baravelli', 'd8b74df393528d51cd19980ae0aa028e'); x instanceof User; // true y instanceof User; // true
上述模式的一个缺点是它须要额外的函数调用,且难适用于可变参数函数,由于没有一种模拟apply方法将可变参数函数做为构造函数调用的方式。
一种更为奇异的方式是利用ES5的Object.create函数。
function User(name, passwordHash) { var self = this instanceof User ? this : Object.create(User.prototype); self.name = name; self.passwordHash = passwordHash; return self; }
Object.create
须要一个原型对象做为参数,并返回一个继承自原型对象的新对象。
多亏了构造函数覆盖模式,使用new操做符调用上述User函数的行为与以函数调用它的行为是同样的,这能工做彻底得益于JavaScript容许new表达式的结果能够被构造函数的显示return语句所覆盖。
提示:
Object.create
方法在构造函数定义中调用自身使得该构造函数与调用语法无关。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(/* ... */);
上述代码中的每一个实例都包含toString和checkPassword方法的副本,而不是经过原型共享这些方法。
将方法存储在原型,使其能够被全部的实例使用,而不须要存储方法实现的多个副本,也不须要给每一个实例对象增长额外的属性。
同时,现代的JavaScript引擎深度优化了原型查找,因此将方法复制到实例对象并不必定保证查找的速度有明显的提高,并且实例方法比起原型方法确定会占用更多的内存。
提示:
任意一段程序均可以简单地经过访问JavaScript对象的属性名来获取相应地对象属性,例如for in
循环、ES5的Object.keys
函数和Object.getOwnPropertyNames
函数。
一些程序员使用命名规范给私有属性前置或后置一个下划线字符_。
然而实际上,一些程序须要更高程度的信息隐藏。
对于这种情形,JavaScript为信息隐藏提供了闭包。闭包将数据存储到封闭的变量中而不提供对这些变量的直接访问,获取闭包内部结构的惟一方式是该函数显式地提供获取它的途径。
利用这一特性在对象中存储真正的私有数据。不是将数据做为对象的属性来存储,而是在构造函数中以变量的方式存储它。
function User(name, passwordHash) { this.toString = function () { return '[User ' + name + ']'; }; this.checkPassword = function (password) { return hash(password) === passwordHash; } }
上述代码的toString和checkPassword方法是以变量的方式来引用name和passwordHash变量的,而不是以this属性的方式来引用,User的实例不包含任何实例属性,所以外部的代码不能直接访问User实例的name和passwordHash变量。
该模式的一个缺点是,为了让构造函数中的变量在使用它们的方法的做用域内,这些方法必须放置于实例对象中,这会致使方法副本的扩散。
提示:
一种错误的作法是不当心将每一个实例的数据存储到了其原型中。
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]
每次调用addChild方法,都会将值添加到Tree.prototype.children数组中。
实现Tree类的正确方式是为每一个实例对象建立一个单独的children数组。
function Tree(x) { this.value = x; this.children = []; // instance state } Tree.prototype = { addChild: function(x) { this.children.push(x); } };
通常状况下,任何不可变的数据能够被存储在原型中从而被安全地共享。有状态的数据原则上也能够存储在原型中,只要你真正想共享它。然而迄今为止,在原型对象中最多见的数据是方法,而每一个实例的状态都存储在实例对象中。
提示:
编写一个简单的、可定制的读取CSV(逗号分隔型取值)数据的类。
function CSVReader(separators) { this.separators = separators || [',']; this.regexp = new RegExp(this.separators.map(function (sep) { return '\\' + sep[0]; }).join('|')); }
实现一个简单的read方法能够分为两步来处理。第一步,将输入的字符串分为按行划分的数组。第二步,将数组的每一行再分为按单元格划分的数组。结果得到一个二维的字符串数组。
CSVReader.prototype.read = function (str) { var lines = str.trim().split(/\n/); return lines.map(function (line) { return line.split(this.regexp); }); }; var reader = new CSVReader(); reader.read('a, b, c\nd, e, f\n'); // [['a, b, c'], ['d, e, f']]
上述代码的bug是,传递给line.map
的回调函数引用的this指向的是window,所以,this.regexp
产生undefined值。
备注:'a, b, c'.split(undefined)
返回['a, b, c']
。
幸运的是,数组的map方法能够传入一个可选的参数做为其回调函数的this绑定。
CSVReader.prototype.read = function (str) { var lines = str.trim().split(/\n/); return lines.map(function (line) { return line.split(this.regexp); }, this); }; var reader = new CSVReader(); reader.read('a, b, c\nd, e, f\n'); // [['a', 'b', 'c'], ['d', 'e', 'f']]
可是,不是全部基于回调函数的API都考虑周全。另外一种解决方案是使用词法做用域的变量来存储这个额外的外部this绑定的引用。
CSVReader.prototype.read = function (str) { var lines = str.trim().split(/\n/); var self = this; // save a reference to outer this-binding return lines.map(function (line) { return line.split(this.regexp); }); }; var reader = new CSVReader(); reader.read('a, b, c\nd, e, f\n'); // [['a', 'b', 'c'], ['d', 'e', 'f']]
在ES5的环境中,另外一种有效的方法是使用回调函数的bind方法。
CSVReader.prototype.read = function (str) { var lines = str.trim().split(/\n/); return lines.map(function (line) { return line.split(this.regexp); }.bind(this)); // bind to outer this-binding }; var reader = new CSVReader(); reader.read('a, b, c\nd, e, f\n'); // [['a', 'b', 'c'], ['d', 'e', 'f']]
提示:
场景图(scene graph)是在可视化的程序中(如游戏或图形仿真场景)描述一个场景的对象集合。一个简单的场景包含了在该场景中的全部对象(称为角色),以及全部角色的预加载图像数据集,还包含一个底层图形显示的引用(一般被称为context)。
function Scene(context, width, height, images) { this.context = context; this.width = width; this.height = height; this.images = images; this.actors = []; } Scene.prototype.register = function (actor) { this.actors.push(actor); }; Scene.prototype.unregister = function (actor) { var i = this.actors.indexOf(actor); if (i >= 0) { this.actors.splice(i, 1); } }; Scene.prototype.draw = function () { this.context.clearRect(0, 0, this.width, this.height); for (var a = this.actors, i = 0, n = a.length; i < n; i++) { a[i].draw(); } };
场景中的全部角色都继承自基类Actor。
function Actor(scene, x, y) { this.scene = scene; this.x = x; this.y = y; scene.register(this); } Actor.prototype.moveTo = function (x, y) { this.x = x; this.y = y; this.scene.draw(); }; Actor.prototype.exit = function() { this.scene.unregister(this); this.scene.draw(); }; Actor.prototype.draw = function () { var image = this.scene.images[this.type]; this.scene.context.drawImage(image, this.x, this.y); }; Actor.prototype.width = function () { return this.scene.images[this.type].width; }; Actor.prototype.height = function () { return this.scene.images[this.type].height; };
咱们将角色的特定类型实现为Actor的子类。例如,在街机游戏中太空飞船就会有一个拓展自Actor的SpaceShip类。
为了确保SpaceShip的实例能做为角色被正确地初始化,其构造函数必须显式地调用Actor的构造函数。经过将接收者绑定到该新对象来调用Actor能够达到此目的。
function SpaceShip(scene, x, y) { Actor.call(this, scene, x, y); this.points = 0; }
调用Actor的构造函数能确保Actor建立的全部实例属性都被添加到了新对象(SpaceShip实例对象)中。为了使SpaceShip成为Actor的一个正确地子类,其原型必须继承自Actor.prototype
。作这种拓展的最好的方式是使用ES5提供的Object.create
方法。
SpaceShip.prototype = Object.create(Actor.prototype);
一旦建立了SpaceShip的原型对象,咱们就能够向其添加全部的可被实例共享的属性。
SpaceShip.prototype.type = 'spaceShip'; SpaceShip.prototype.scorePoint = function () { this.points++; }; SpaceShip.prototype.left = function () { this.moveTo(Math.max(this.x - 10, 0), this.y); }; SpaceShip.prototype.right = function () { var maxWidth = this.scene.width - this.width(); this.moveTo(Math.min(this.x + 10, maxWidth), this.y); };
提示:
Object.create
函数来构造子类的原型对象以免调用父类的构造函数。function Actor(scene, x, y) { this.scene = scene; this.x = x; this.y = y; this.id = ++Actor.nextID; scene.register(this); } Actor.nextID = 0;
function Alien(scene, x, y, direction, speed, strength) { Actor.call(this, scene, x, y); this.direction = direction; this.speed = speed; this.strength = strength; this.damage = 0; this.id = ++Alien.nextID; // conflicts with actor id! } Alien.nextID = 0;
Alien类与其父类Actor类都视图给实例属性id写数据。若是在继承体系中的两个类指向相同的属性名,那么它们指向的是同一个属性。
该例子显而易见的解决方法是对Actor标识数和Alien标识数使用不一样的属性名。
function Actor(scene, x, y) { this.scene = scene; this.x = x; this.y = y; this.actorID = ++Actor.nextID; // distinct from alienID scene.register(this); } Actor.nextID = 0; function Alien(scene, x, y, direction, speed, strength) { Actor.call(this, scene, x, y); this.direction = direction; this.speed = speed; this.strength = strength; this.damage = 0; this.alienID = ++Alien.nextID; // distinct from actorID } Alien.nextID = 0;
提示:
一个操做文件系统的库可能但愿建立一个抽象的目录,该目录继承了数组的全部行为。
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
遗憾的是,这种方式破坏了数组的length属性的预期行为。
var dir = new Dir('/tmp/mysite', ['index.html', 'script.js', 'style.css']); dir.length; // 0
失败的缘由是length属性只对在内部标记为“真正的”数组的特殊对象起做用。ECMAScript标准规定它是一个不可见的内部属性,称为[[Class]]。
数组对象(经过Array构造函数或[]语法建立)被加上了值为“Array”的[[Class]]属性,函数被加上了值为“Function”的[[Class]]属性。
事实证实,length的行为只被定义在内部属性[[Class]]的值为“Array”的特殊对象中。对于这些对象,JavaScript保持length属性与该对象的索引属性的数量同步。
但当咱们拓展Array类时,子类的实例并非经过new Array()
或字面量[]语法建立的。因此,Dir的实例[[Class]]属性值为“Object”。
更好的实现是定义一个entries数组的实例属性。
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); };
提示:
原型是一种对象行为的实现细节。
JavaScript提供了便利的内省机制(introspection mechanisms)来检查对象的细节。Object.prototype.hasOwnProperty
方法肯定一个属性是否为对象“本身的”属性(即一个实例属性),而彻底忽略原型继承机构。Object.getPrototypeOf
和__proto__
特性容许程序员遍历对象的原型链并单独查询其原型对象。
检查实现细节(即便没有修改它们)也会在程序的组件之间建立依赖。若是对象的生产者修改了实现细节,那么依赖于这些对象的使用者就会被破坏。
提示:
因为对象共享原型,所以每个对象均可以增长、删除或修改原型的属性,这个有争议的实践一般被称为猴子补丁(monkey-patching)。
猴子补丁的吸引力在于它的强大,数组缺乏一个有用的方法,你本身就能够增长它。
Array.prototype.split = function (i) { // alternative #1 return [this.slice(0, 1), this.slice(i)]; };
可是当多个库以不兼容的方式给同一个原型打猴子补丁时,问题就出现了。
Array.prototype.split = function (i) { // alternative #2 var i = Math.floor(this.length / 2); return [this.slice(0, 1), this.slice(i)]; };
如今,任一对数组split方法的使用都大约有50%的机会被破坏。
一个方法能够将这些修改置于一个函数中,用户能够选择调用或忽略。
function addArrayMethods() { Array.prototype.split = function (i) { return [this.slice(0, 1), this.slice(i)]; } }
尽管猴子补丁很危险,可是有一种特别可靠并且有价值的使用场景:polyfill。
if (typeof Array.prototype.map !== 'function') { Array.prototype.map = function (f, thisArg) { var result = []; for (var i = 0, n = this.length; i < n; i++) { result[i] = f.call(thisArg, this[i], i); } return result; }; }
提示: