这是我的一点点感悟:
感悟1:关于魔术方法foreach
foreach方法的定义,有的需要context,有的不需要,这是看你是否需要使用call/applay,如果是的话就需要给call/apply提供一个context参数,反之就不需要提供了。
在Range的定义里面没有使用context参数,而在例9-6和例9-7都使用了context参数
感悟2:javascript的动态性
他的变量是无类型的,或者说可以随时被赋予另外一种类型的值。
java对象的动态性在于:他的属性可以动态添加删除替换,这里的属性包括字段和方法。
我们有时候觉得这个很有趣,但是我们很多时候也不需要这么动态,我们经常希望类的属性定义好以后不会被替换或者删除,当然这种希望最好是强制性的,而不仅仅是美好的愿望。
感悟3:如何把真正的面向对象特性赋予给javascript?
javascript在保持动态性的同时,又想要拥有真正的面向对象编程语言如java那样的比较完善的封装性/继承性/多态性,就要考虑做很多手脚,下面的内容就是基于此做出了的。
如果不是好好看过这些,我们一开始看到人家封装的javascript类可能觉得莫名其妙不知所措,这种恐慌来自于知其然不知其所以然,甚至于连知然都不是那么容易判断。
所以这一部分的内容很重要。
下面开始正式学习javascript的类和模块
1.类和模块
1.1.类和原型
必须认识到,js的类的实现是基于原型继承机制的。如果两个实例都从同一个原型对象上继承了属性,我们说它们是同一个类的实例。
在js中,原型对象是类的核心。
Example 6-1. Creating a new object that inherits from a prototype
// inherit() returns a newly created object that inherits properties from the
// prototype object p. It uses the ECMAScript 5 function Object.create() if
// it is defined, and otherwise falls back to an older technique.
function inherit(p) {
if (p == null) throw TypeError(); // p must be a non-null object
if (Object.create) // If Object.create() is defined...
return Object.create(p); // then just use it.
var t = typeof p; // Otherwise do some more type checking
if (t !== "object" && t !== "function") throw TypeError();
function f() {}; // Define a dummy constructor function.
f.prototype = p; // Set its prototype property to p.
return new f(); // Use f() to create an "heir" of p.
}
Example 9-1. A simple JavaScript class
// range.js: A class representing a range of values.
// This is a factory function that returns a new range object. 工厂方法产生JS类
function range(from, to) {
// Use the inherit() function to create an object that inherits from the
// prototype object defined below. The prototype object is stored as
// a property of this function, and defines the shared methods (behavior)
// for all range objects.
var r = inherit(range.methods);
// Store the start and end points (state) of this new range object.
// These are noninherited properties that are unique to this object.
r.from = from;
r.to = to;
// Finally return the new object
return r;
}
// This prototype object defines methods inherited by all range objects.
range.methods = {
// Return true if x is in the range, false otherwise
// This method works for textual and Date ranges as well as numeric.
includes: function(x) { return this.from <= x && x <= this.to; },
// Invoke f once for each integer in the range.
// This method works only for numeric ranges.
foreach: function(f) {
for(var x = Math.ceil(this.from); x <= this.to; x++) f(x);
},
// Return a string representation of the range
toString: function() { return "(" + this.from + "..." + this.to + ")"; }
};
// Here are example uses of a range object.
var r = range(1,3); // Create a range object
r.includes(2); // => true: 2 is in the range
r.foreach(console.log); // Prints 1 2 3
console.log(r); // Prints (1...3) 实际上打印了:{ from: 1, to: 3 }
r.toString();//打印了:(1...3)
r;//打印了:{ from: 1, to: 3 }
1.2.类和构造函数
例9-1展示了在js中定义类的一种方法-工厂函数,但是最常见的还是使用构造函数。
构造函数使得我找到一点点类的感觉,例9-1那样的能叫做类吗?嘿嘿
Example 9-2. A Range class using a constructor 使用构造函数的Range类
// range2.js: Another class representing a range of values.
// This is a constructor function that initializes new Range objects.
// Note that it does not create or return the object. It just initializes this.
function Range(from, to) {
// Store the start and end points (state) of this new range object.
// These are noninherited properties that are unique to this object.
this.from = from;
this.to = to;
}
// All Range objects inherit from this object.
// Note that the property name must be "prototype" for this to work.
Range.prototype = {
// Return true if x is in the range, false otherwise
// This method works for textual and Date ranges as well as numeric.
includes: function(x) { return this.from <= x && x <= this.to; },
// Invoke f once for each integer in the range.
// This method works only for numeric ranges.
foreach: function(f) {
for(var x = Math.ceil(this.from); x <= this.to; x++) f(x);
},
// Return a string representation of the range
toString: function() { return "(" + this.from + "..." + this.to + ")"; }
};
// Here are example uses of a range object
var r = new Range(1,3); // Create a range object
r.includes(2); // => true: 2 is in the range
r.foreach(console.log); // Prints 1 2 3
console.log(r); // Prints (1...3) 实际上打印了:{ from: 1, to: 3 }
r.toString();//打印了:(1...3)
r;//打印了:{ from: 1, to: 3 }
1.2.1.构造函数和类的标识
类的名字大写字母开始,这和一般的工厂函数不同。不同之处还在于构造函数使用new 关键字来调用。
第三个不同之处,是原型对象的不同,工厂函数的原型定义比较随意,而构造函数的原型必须是prototype。我们也看不到构造函数有代码去继承了prototype里面的属性,因为这是约定好的。
原型对象是类的唯一标识:当且仅当两个对象继承自同一个原型对象时,他们才是属于同一个类的实例。
我原先以为构造函数是类的唯一标识,但是其实构造函数是类的公共标识。
事实上,两个不同的构造函数可以使用同一个原型对象(prototyp属性指向),那么这两个不同的构造函数创建的实例都是属于同一个类的
构造函数是类的“外在表现”,构造函数的名字通常用做类名,使用instanceof运算符就是会用到构造函数。
r instanceof Range// true
1.2.2.constructor属性
var F = function() {}; // This is a function object.
var p = F.prototype; // This is the prototype object associated with it.
var c = p.constructor; // This is the function associated with the prototype.
c === F // => true: F.prototype.constructor==F for any function
就是说,我们从构造函数可以找到类的原型对象,从原型对象也可以找到构造函数。
例9-2中的prototype没有定义constructor属性。
我们有两种解决方法。
方法1:在新的prototype对象中加入constructor属性
Range.prototype = {
constructor: Range, // Explicitly set the constructor back-reference
includes: function(x) { return this.from <= x && x <= this.to; },
foreach: function(f) {
for(var x = Math.ceil(this.from); x <= this.to; x++) f(x);
},
toString: function() { return "(" + this.from + "..." + this.to + ")"; }
};
方法2:在原有的预定义的prototype对象基础上面增加我们的各种属性,每次加一个属性。
// Extend the predefined Range.prototype object so we don't overwrite
// the automatically created Range.prototype.constructor property.
Range.prototype.includes = function(x) { return this.from<=x && x<=this.to; };
Range.prototype.foreach = function(f) {
for(var x = Math.ceil(this.from); x <= this.to; x++) f(x);
};
Range.prototype.toString = function() {
return "(" + this.from + "..." + this.to + ")";
};
1.3.javascript中java式的类继承
java式的类,具有类字段、类方法、实例字段、实例方法。
js也可以模拟java式的类。
实例字段、类字段和类方法都可以直接定义为构造函数的属性;而实例方法需要放到prototype对象中去。
Example 9-3. Complex.js: A complex number class
/*
* Complex.js:
* This file defines a Complex class to represent complex numbers.
* Recall that a complex number is the sum of a real number and an
* imaginary number and that the imaginary number i is the square root of -1.
*/
/*
* This constructor function defines the instance fields r and i on every
* instance it creates. These fields hold the real and imaginary parts of
* the complex number: they are the state of the object.
* 实例字段
*/
function Complex(real, imaginary) {
if (isNaN(real) || isNaN(imaginary)) // Ensure that both args are numbers.
throw new TypeError(); // Throw an error if they are not.
this.r = real; // The real part of the complex number.
this.i = imaginary; // The imaginary part of the number.
}
/*
* The instance methods of a class are defined as function-valued properties
* of the prototype object. The methods defined here are inherited by all
* instances and provide the shared behavior of the class. Note that JavaScript
* instance methods must use the this keyword to access the instance fields.
* 实例方法作为prototype对象的函数值属性。实例方法使用this关键字访问实例字段
*/
// Add a complex number to this one and return the sum in a new object.
Complex.prototype.add = function(that) {
return new Complex(this.r + that.r, this.i + that.i);
};
// Multiply this complex number by another and return the product.
Complex.prototype.mul = function(that) {
return new Complex(this.r * that.r - this.i * that.i,
this.r * that.i + this.i * that.r);
};
// Return the real magnitude of a complex number. This is defined
// as its distance from the origin (0,0) of the complex plane.
Complex.prototype.mag = function() {
return Math.sqrt(this.r*this.r + this.i*this.i);
};
// Return a complex number that is the negative of this one.
Complex.prototype.neg = function() { return new Complex(-this.r, -this.i); };
// Convert a Complex object to a string in a useful way.
Complex.prototype.toString = function() {
return "{" + this.r + "," + this.i + "}";
};
// Test whether this Complex object has the same value as another.
Complex.prototype.equals = function(that) {
return that != null && // must be defined and non-null
that.constructor === Complex && // and an instance of Complex
this.r === that.r && this.i === that.i; // and have the same values.
};
/*
* Class fields (such as constants) and class methods are defined as
* properties of the constructor. Note that class methods do not
* generally use the this keyword: they operate only on their arguments.
* 类字段(常量)定义为构造函数的属性
*/
// Here are some class fields that hold useful predefined complex numbers.
// Their names are uppercase to indicate that they are constants.
// (In ECMAScript 5, we could actually make these properties read-only.)
Complex.ZERO = new Complex(0,0);
Complex.ONE = new Complex(1,0);
Complex.I = new Complex(0,1);
// This class method parses a string in the format returned by the toString
// instance method and returns a Complex object or throws a TypeError.
//类方法也是构造函数的属性
Complex.parse = function(s) {
try { // Assume that the parsing will succeed
var m = Complex._format.exec(s); // Regular expression magic
return new Complex(parseFloat(m[1]), parseFloat(m[2]));
} catch (x) { // And throw an exception if it fails
throw new TypeError("Can't parse '" + s + "' as a complex number.");
}
};
// A "private" class field used in Complex.parse() above.
// The underscore in its name indicates that it is intended for internal
// use and should not be considered part of the public API of this class.
Complex._format = /^\{([^,]+),([^}]+)\}$/;
//测试上面定义的类Complex
var c = new Complex(2,3); // Create a new object with the constructor
var d = new Complex(c.i,c.r); // Use instance properties of c (3,2)
c.add(d).toString(); // => "{5,5}": use instance methods
// A more complex expression that uses a class method and field
Complex.parse(c.toString()). // Convert c to a string and back again,
add(c.neg()). // add its negative to it,
equals(Complex.ZERO) // and it will always equal zero
注意:js和java不同,引用实例字段必须使用关键字this。
java中有final声明常量,可以将字段和方法定义为private。
私有属性可以使用闭包里的局部变量来模拟,常量属性可以再ECMASCRIPT5中直接实现。
1.4.类的扩充
js基于原型的继承机制是动态的:对象从其原型继承属性,如果创建对象实例以后原型的属性发生了改变,也会影响到继承这个原型的所有实例对象。
这意味着我们可以给原型对象添加新方法来扩充javascript类。
下面的代码给上面定义的Complex添加一个方法,用来计算复数的共轭复数。
// Return a complex number that is the complex conjugate of this one.
Complex.prototype.conj = function() { return new Complex(this.r, -this.i); };
下面 是其他的例子
// Invoke the function f this many times, passing the iteration number
// For example, to print "hello" 3 times:
// var n = 3;
// n.times(function(n) { console.log(n + " hello"); });
Number.prototype.times = function(f, context) {
var n = Number(this);
for(var i = 0; i < n; i++) f.call(context, i);
};
// Define the ES5 String.trim() method if one does not already exist.
// This method returns a string with whitespace removed from the start and end.
String.prototype.trim = String.prototype.trim || function() {
if (!this) return this; // Don't alter the empty string
return this.replace(/^\s+|\s+$/g, ""); // Regular expression magic
};
// Return a function's name. If it has a (nonstandard) name property, use it.
// Otherwise, convert the function to a string and extract the name from that.
// Returns an empty string for unnamed functions like itself.
Function.prototype.getName = function() {
return this.name || this.toString().match(/function\s*([^(]*)\(/)[1];
};
可以给已有的内置的对象或者对象的原型添加方法,但是这些新增的方法需要修改为不可枚举的(ECMASCRIPT5已经可以这么做)
还有一个方法-Object.defineProperty()方法可以安全的扩充Object.prototype
很多浏览器可以给HTMLElement.prototype添加方法,但是IE不能。
1.5.类和类型
一般的内置对象,我们可以通过typeof运算符、class属性、classof函数识别类型。
Example 6-4. A classof() function
function classof(o) {
if (o === null) return "Null";
if (o === undefined) return "Undefined";
return Object.prototype.toString.call(o).slice(8,-1);
}
但是对于我们自定义的类呢?他们的class属性都是"Object",这时候classof()函数也无用武之地。怎么判断自定义类的类型呢?我们喜欢知道某个对象的类名(构造函数的名称),原型对象的名称,甚至这个类型的能力(更加关注能够做什么有什么方法,这种编程哲学叫做duck-typing)
下面会讲到4种测试对象类型的方法:
instanceof运算符
constructor属性
构造函数的名字
duck-typing
1.5.1.instanceof运算符
o instanceof ClassName
这里的ClassName就是构造函数名称
> r instanceof Range
true
> r1=range(1,5);
{ from: 1, to: 5 }
> r1
{ from: 1, to: 5 }
> r1.toString()
'(1...5)'
> r1.includes(3)
true
> r1.foreach(console.log)
1
2
3
4
5
undefined
> console.log(r1)
{ from: 1, to: 5 }
undefined
> r1 instanceof range
false
> r1 instanceof Range
false
> range.methods.isPrototypeOf(r1)
true
除了instanceof运算符,还可以利用 isPrototypeOf()函数测试原型对象,这就是不使用构造函数作为中介的一种方法。
缺点:
a. 无法获得一个对象的类名
b. 在web客户端js中,由于多窗口和多框架子页面,每个窗口和框架子页面都有单独的执行上下文,每个上下文都含有独有的全局变量和一组构造函数。
一个窗口里面的Array构造函数和另外一个窗口里面的Array构造函数不是同一个构造函数,虽然他们实际上是一样的,仅仅是同一个构造函数的副本而已。
1.5.2.constructor属性
function typeAndValue(x) {
if (x == null) return ""; // Null and undefined don't have constructors
switch(x.constructor) {
case Number: return "Number: " + x; // Works for primitive types
case String: return "String: '" + x + "'";
case Date: return "Date: " + x; // And for built-in types
case RegExp: return "Regexp: " + x;
case Complex: return "Complex: " + x; // And for user-defined types
}
}
缺点和instanceof一样,在web多窗口环境下面不好用。
同样也不是所有js对象都有constructor属性。
原型上面的constructor属性经常被我们忽略了。
1.5.3.构造函数的名称
下面我们想方设法得到一个构造函数的名称
Example 9-4. A type() function to determine the type of a value
/**
* Return the type of o as a string:
* -If o is null, return "null", if o is NaN, return "nan".
* -If typeof returns a value other than "object" return that value.
* (Note that some implementations identify regexps as functions.)
* -If the class of o is anything other than "Object", return that.
* -If o has a constructor and that constructor has a name, return it.
* -Otherwise, just return "Object".
**/
function type(o) {
var t, c, n; // type, class, name
// Special case for the null value:
if (o === null) return "null";
// Another special case: NaN is the only value not equal to itself:
if (o !== o) return "nan";
// Use typeof for any value other than "object".
// This identifies any primitive value and also functions.
if ((t = typeof o) !== "object") return t;
// Return the class of the object unless it is "Object".
// This will identify most native objects.
if ((c = classof(o)) !== "Object") return c;
// Return the object's constructor name, if it has one
if (o.constructor && typeof o.constructor === "function" &&
(n = o.constructor.getName())) return n;
// We can't determine a more specific type, so return "Object"
return "Object";
}
// Return the class of an object.
function classof(o) {
return Object.prototype.toString.call(o).slice(8,-1);
};
// Return the name of a function (may be "") or null for nonfunctions
Function.prototype.getName = function() {
if ("name" in this) return this.name;
return this.name = this.toString().match(/function\s*([^(]*)\(/)[1];
};
但是这种方法也有一个缺点:
不是所有函数都有名字,如果使用一个匿名的函数定义表达式定义了一个构造函数,我们就只能知道他的名字是空字符串。
// This constructor has no name 这个构造函数没有名字
var Complex = function(x,y) { this.r = x; this.i = y; }
// This constructor does have a name 这个构造函数有一个名字
var Range = function Range(f,t) { this.from = f; this.to = t; }
1.5.4.duck-typing
像鸭子一样走路、游泳并且嘎嘎叫的鸟,就是鸭子。
When I see a bird that walks like a duck and swims like a duck and quacks like a duck,
I call that bird a duck.
Example 9-5. A function for duck-type checking
// Return true if o implements the methods specified by the remaining args.
function quacks(o /*, ... */) {
for(var i = 1; i < arguments.length; i++) { // for each argument after o
var arg = arguments[i];
switch(typeof arg) { // If arg is a:
case 'string': // string: check for a method with that name 字符串,就直接检查是否对象O含有该函数
if (typeof o[arg] !== "function") return false;
continue;
case 'function': // function: use the prototype object instead 函数类型,使用他的原型对象
// If the argument is a function, we use its prototype object
arg = arg.prototype;
// fall through to the next case
case 'object': // object: check for matching methods 如果是对象,检查他的方法是否在对象O里面都实现了
for(var m in arg) { // For each property of the object
if (typeof arg[m] !== "function") continue; // skip non-methods
if (typeof o[m] !== "function") return false;
}
}
}
// If we're still here, then o implements everything,O实现了所有的方法,就返回true
return true;
}
上面这个方法应用于内置类有些问题,因为内置类的方法不可枚举,上面使用了for/in遍历。
ECMAScript 5的JS实现,可以考虑使用 Object.getOwnPropertyNames()去遍历内置类的方法。
1.6.JS中的面向对象技术
看两个经典对象的例子,可以学到不少东西
1.6.1.set对象的例子
Example 9-6. Set.js: An arbitrary set of values
function Set() { // This is the constructor 构造函数
this.values = {}; // The properties of this object hold the set
this.n = 0; // How many values are in the set
this.add.apply(this, arguments); // All arguments are values to add
}
//下面在prototype上面定义了一系列的实例方法
// Add each of the arguments to the set.
Set.prototype.add = function() {
for(var i = 0; i < arguments.length; i++) { // For each argument
var val = arguments[i]; // The value to add to the set
var str = Set._v2s(val); // Transform it to a string
if (!this.values.hasOwnProperty(str)) { // If not already in the set
this.values[str] = val; // Map string to value
this.n++; // Increase set size
}
}
return this; // Support chained method calls
};
// Remove each of the arguments from the set.
Set.prototype.remove = function() {
for(var i = 0; i < arguments.length; i++) { // For each argument
var str = Set._v2s(arguments[i]); // Map to a string
if (this.values.hasOwnProperty(str)) { // If it is in the set
delete this.values[str]; // Delete it
this.n--; // Decrease set size
}
}
return this; // For method chaining
};
// Return true if the set contains value; false otherwise.
Set.prototype.contains = function(value) {
return this.values.hasOwnProperty(Set._v2s(value));
};
// Return the size of the set.
Set.prototype.size = function() { return this.n; };
// Call function f on the specified context for each element of the set.
Set.prototype.foreach = function(f, context) {
for(var s in this.values) // For each string in the set
if (this.values.hasOwnProperty(s)) // Ignore inherited properties
f.call(context, this.values[s]); // Call f on the value
};
// This internal function maps any JavaScript value to a unique string.
Set._v2s = function(val) {
switch(val) {
case undefined: return 'u'; // Special primitive
case null: return 'n'; // values get single-letter
case true: return 't'; // codes.
case false: return 'f';
default: switch(typeof val) {
case 'number': return '#' + val; // Numbers get # prefix.
case 'string': return '"' + val; // Strings get " prefix.
default: return '@' + objectId(val); // Objs and funcs get @
}
}
// For any object, return a string. This function will return a different
// string for different objects, and will always return the same string
// if called multiple times for the same object. To do this it creates a
// property on o. In ES5 the property would be nonenumerable and read-only.
function objectId(o) {
// Private property name for storing ids
//???一开始我没看懂,看到后面才目标这是一种特殊属性,后面会看到这种特殊的属性-ID值在equals函数里面忽略了
var prop = "|**objectid**|";
if (!o.hasOwnProperty(prop)) // If the object has no id
o[prop] = Set._v2s.next++; // Assign it the next available
return o[prop]; // Return the id
}
};
Set._v2s.next = 100; // Start assigning object ids at this value.
下面是我们的测试,可以和后面1.8,1.9学过私有状态和不可枚举属性做一个对比
用来做对比的是例9-10的PrivateRange, 例9-20的ImmutableRange
> var o=new Set(1,2,4,55);
undefined
> o
{ values:
{ '#1': 1,
'#2': 2,
'#4': 4,
'#55': 55 },
n: 4 }
> for (p in o) console.log(p);
values
n
add
remove
contains
size
foreach
undefined
> for (p in o) console.log(p,o[p]);
values { '#1': 1, '#2': 2, '#4': 4, '#55': 55 }
n 4
add function () {
for(var i = 0; i < arguments.length; i++) { // For each argument
var val = arguments[i]; // The value to add to the set
var str = Set._v2s(val); // Transform it to a string
if (!this.values.hasOwnProperty(str)) { // If not already in the set
this.values[str] = val; // Map string to value
this.n++; // Increase set size
}
}
return this; // Support chained method calls
}
remove function () {
for(var i = 0; i < arguments.length; i++) { // For each argument
var str = Set._v2s(arguments[i]); // Map to a string
if (this.values.hasOwnProperty(str)) { // If it is in the set
delete this.values[str]; // Delete it
this.n--; // Decrease set size
}
}
return this; // For method chaining
}
contains function (value) {
return this.values.hasOwnProperty(Set._v2s(value));
}
size function () { return this.n; }
foreach function (f, context) {
for(var s in this.values) // For each string in the set
if (this.values.hasOwnProperty(s)) // Ignore inherited properties
f.call(context, this.values[s]); // Call f on the value
}
undefined
> for (p in Set) console.log(p,o[p]);
_v2s undefined
1.6.2.枚举类型Enumerated的例子
Example 9-7. Enumerated types in JavaScript
// This function creates a new enumerated type. The argument object specifies
// the names and values of each instance of the class. The return value
// is a constructor function that identifies the new class. Note, however
// that the constructor throws an exception: you can't use it to create new
// instances of the type. The returned constructor has properties that
// map the name of a value to the value itself, and also a values array,
// a foreach() iterator function
function enumeration(namesToValues) {
// This is the dummy constructor function that will be the return value.
var enumeration = function() { throw "Can't Instantiate Enumerations"; };//构造函数的作用仅仅是报错
// Enumerated values inherit from this object.
var proto = enumeration.prototype = { //在这里定义实例方法,实例字段
constructor: enumeration, // Identify type
toString: function() { return this.name; }, // Return name
valueOf: function() { return this.value; }, // Return value
toJSON: function() { return this.name; } // For serialization
};
enumeration.values = []; // An array of the enumerated value objects
// Now create the instances of this new type.
for(name in namesToValues) { // For each value
var e = inherit(proto); // Create an object to represent it
e.name = name; // Give it a name
e.value = namesToValues[name]; // And a value
enumeration[name] = e; // Make it a property of constructor
enumeration.values.push(e); // And store in the values array
}
// A class method for iterating the instances of the class 类方法
enumeration.foreach = function(f,context) {
for(var i = 0; i < this.values.length; i++) f.call(context,this.values[i]);
};
// Return the constructor that identifies the new type
return enumeration;
}
//下面是一些enumeration的协议测试
// Create a new Coin class with four values: Coin.Penny, Coin.Nickel, etc.
var Coin = enumeration({Penny: 1, Nickel:5, Dime:10, Quarter:25});
var c = Coin.Dime; // This is an instance of the new class
c instanceof Coin // => true: instanceof works
c.constructor == Coin // => true: constructor property works
Coin.Quarter + 3*Coin.Nickel // => 40: values convert to numbers
Coin.Dime == 10 // => true: more conversion to numbers
Coin.Dime > Coin.Nickel // => true: relational operators work
String(Coin.Dime) + ":" + Coin.Dime // => "Dime:10": coerce to string
> enumeration
[Function: enumeration]
> Coin
{ [Function]
values:
[ { name: 'Penny', value: 1 },
{ name: 'Nickel', value: 5 },
{ name: 'Dime', value: 10 },
{ name: 'Quarter', value: 25 } ],
Penny: { name: 'Penny', value: 1 },
Nickel: { name: 'Nickel', value: 5 },
Dime: { name: 'Dime', value: 10 },
Quarter: { name: 'Quarter', value: 25 },
foreach: [Function] }
> var tt=new Coin()
Can't Instantiate Enumerations
> enumeration.prototype //我们看不到这里面定义的属性
{}
> enumeration.prototype.toString()
'[object Object]'
下面的例子用一个纸牌的例子说明enumeration的使用
Example 9-8. Representing cards with enumerated types 使用枚举类型表示一副poker牌
// Define a class to represent a playing card
function Card(suit, rank) {
this.suit = suit; // Each card has a suit
this.rank = rank; // and a rank
}
// These enumerated types define the suit and rank values
Card.Suit = enumeration({Clubs: 1, Diamonds: 2, Hearts:3, Spades:4});
Card.Rank = enumeration({Two: 2, Three: 3, Four: 4, Five: 5, Six: 6,
Seven: 7, Eight: 8, Nine: 9, Ten: 10,
Jack: 11, Queen: 12, King: 13, Ace: 14});
// Define a textual representation for a card
Card.prototype.toString = function() {
return this.rank.toString() + " of " + this.suit.toString();
};
// Compare the value of two cards as you would in poker
Card.prototype.compareTo = function(that) {
if (this.rank < that.rank) return -1;
if (this.rank > that.rank) return 1;
return 0;
};
// A function for ordering cards as you would in poker
Card.orderByRank = function(a,b) { return a.compareTo(b); };
// A function for ordering cards as you would in bridge
Card.orderBySuit = function(a,b) {
if (a.suit < b.suit) return -1;
if (a.suit > b.suit) return 1;
if (a.rank < b.rank) return -1;
if (a.rank > b.rank) return 1;
return 0;
};
// Define a class to represent a standard deck of cards
function Deck() {
var cards = this.cards = []; // A deck is just an array of cards
Card.Suit.foreach(function(s) { // Initialize the array
Card.Rank.foreach(function(r) {
cards.push(new Card(s,r));
});
});
}
// Shuffle method: shuffles cards in place and returns the deck 洗牌的方法
Deck.prototype.shuffle = function() {
// For each element in the array, swap with a randomly chosen lower element
var deck = this.cards, len = deck.length;
for(var i = len-1; i > 0; i--) {
var r = Math.floor(Math.random()*(i+1)), temp; // Random number
temp = deck[i], deck[i] = deck[r], deck[r] = temp; // Swap
}
return this;
};
// Deal method: returns an array of cards
Deck.prototype.deal = function(n) {
if (this.cards.length < n) throw "Out of cards";
return this.cards.splice(this.cards.length-n, n);
};
// Create a new deck of cards, shuffle it, and deal a bridge hand
var deck = (new Deck()).shuffle();
var hand = deck.deal(13).sort(Card.orderBySuit);
1.6.3.标准转换方法-toString,toLocaleString,toJSON,valueOf
补充上面Set类的标准转化方法
使用了extend来向Set.prototype添加方法
// Add these methods to the Set prototype object.
extend(Set.prototype, {
// Convert a set to a string
toString: function() {
var s = "{", i = 0;
this.foreach(function(v) { s += ((i++ > 0)?", ":"") + v; });
return s + "}";
},
// Like toString, but call toLocaleString on all values
toLocaleString : function() {
var s = "{", i = 0;
this.foreach(function(v) {
if (i++ > 0) s += ", ";
if (v == null) s += v; // null & undefined
else s += v.toLocaleString(); // all others
});
return s + "}";
},
// Convert a set to an array of values
toArray: function() {
var a = [];
this.foreach(function(v) { a.push(v); });
return a;
}
});
// Treat sets like arrays for the purposes of JSON stringification.
Set.prototype.toJSON = Set.prototype.toArray;
1.6.3.比较方法
给前面的Range加上constructor属性和equals方法,以实现两个Range对象实例可以比较。
// The Range class overwrote its constructor property. So add it now.
Range.prototype.constructor = Range;
// A Range is not equal to any nonrange.
// Two ranges are equal if and only if their endpoints are equal.
Range.prototype.equals = function(that) {
if (that == null) return false; // Reject null and undefined
if (that.constructor !== Range) return false; // Reject non-ranges
// Now return true if and only if the two endpoints are equal.
return this.from == that.from && this.to == that.to;
}
Set类的比较方法
Set.prototype.equals = function(that) {
// Shortcut for trivial case
if (this === that) return true;
// If the that object is not a set, it is not equal to this one.
// We use instanceof to allow any subclass of Set.
// We could relax this test if we wanted true duck-typing.
// Or we could strengthen it to check this.constructor == that.constructor
// Note that instanceof properly rejects null and undefined values
if (!(that instanceof Set)) return false;
// If two sets don't have the same size, they're not equal
if (this.size() != that.size()) return false;
// Now check whether every element in this is also in that.
// Use an exception to break out of the foreach if the sets are not equal.
try {
this.foreach(function(v) { if (!that.contains(v)) throw false; });
return true; // All elements matched: sets are equal.
} catch (x) {
if (x === false) return false; // An element in this is not in that.
throw x; // Some other exception: rethrow it.
}
};
我们经常处理这种数组,数组额元素都是同一类的实例。数组元素的排序也是我们经常要做的操作。
Array.sort()有一个参数是排序的方法。
参考了java语言,我们在类里面也实现compareTo方法,这样就可以用在sort中了。
下面是Range的compareTo方法。
// Order ranges by lower bound, or upper bound if the lower bounds are equal.
// Throws an error if passed a non-Range value.
// Returns 0 if and only if this.equals(that).
Range.prototype.compareTo = function(that) {
if (!(that instanceof Range))
throw new Error("Can't compare a Range with " + that);
var diff = this.from - that.from; // Compare lower bounds
if (diff == 0) diff = this.to - that.to; // If equal, compare upper bounds
return diff;
};
//在Range实例的数组中,我们使用了compareTo方法
ranges.sort(function(a,b) { return a.compareTo(b); });
//或者先定义好一个Range的方法,然后再sort中使用,这个其实也暗示-我们同一个类可以有几个比较方法
Range.byLowerBound = function(a,b) { return a.compareTo(b); };
ranges.sort(Range.byLowerBound);
1.6.5.方法借用-javascript中的多重继承
类数组类的方法可以借用Array.prototype中的方法。
也可以使用泛型方法(generic method)
Example 9-9. Generic methods for borrowing
var generic = {
// Returns a string that includes the name of the constructor function
// if available and the names and values of all noninherited, nonfunction
// properties.
toString: function() {
var s = '[';
// If the object has a constructor and the constructor has a name,
// use that class name as part of the returned string. Note that
// the name property of functions is nonstandard and not supported
// everywhere.
if (this.constructor && this.constructor.name)
s += this.constructor.name + ": ";
// Now enumerate all noninherited, nonfunction properties
var n = 0;
for(var name in this) {
if (!this.hasOwnProperty(name)) continue; // skip inherited props
var value = this[name];
if (typeof value === "function") continue; // skip methods
if (n++) s += ", ";
s += name + '=' + value;
}
return s + ']';
},
// Tests for equality by comparing the constructors and instance properties
// of this and that. Only works for classes whose instance properties are
// primitive values that can be compared with ===.
// As a special case, ignore the special property added by the Set class.
equals: function(that) {
if (that == null) return false;
if (this.constructor !== that.constructor) return false;
for(var name in this) {
if (name === "|**objectid**|") continue; // skip special prop.
if (!this.hasOwnProperty(name)) continue; // skip inherited
if (this[name] !== that[name]) return false; // compare values
}
return true; // If all properties matched, objects are equal.
}
};
//Range类里面可以直接借用上面的泛型方法
//是不是发现javascript也很可爱的,很方便吧。
//只要没有被她的所谓魔术所吓倒 :)
Range.prototype.equals = generic.equals;
1.6.6.私有状态
为了模拟java类的私有状态,我们把原先的实例字段变成实例方法,无法绕开实例方法去存取该私有状态
也就是说在构造函数中定义一个函数,然后把这个函数赋值给类的一个属性。
Example 9-10. PrivateRange- A Range class with weakly encapsulated endpoints
Range类的简单包装-私有状态的读取器(使用了闭包)
为了区别于其他的Range类定义,我们把这个使用了私有状态存取器的类叫做PrivateRange
function Range(from, to) {
// Don't store the endpoints as properties of this object. Instead
// define accessor functions that return the endpoint values.
// These values are stored in the closure.
this.from = function() { return from; };
this.to = function() { return to; };
}
// The methods on the prototype can't see the endpoints directly: they have
// to invoke the accessor methods just like everyone else.
Range.prototype = {
constructor: Range,
includes: function(x) { return this.from() <= x && x <= this.to(); },
foreach: function(f) {
for(var x=Math.ceil(this.from()), max=this.to(); x <= max; x++) f(x);
},
toString: function() { return "(" + this.from() + "..." + this.to() + ")"; }
};
//下面对PrivateRange做测试,测试结果可以和例9-6的Set和例9-20的ImmutableRange做对比
> var pr=new PrivateRange(112,3333)
undefined
> pr
{ from: [Function], to: [Function] }
> for (p in pr)console.log(p);
from
to
constructor
includes
foreach
toString
undefined
> for (p in pr)console.log(p,pr[p]);
from function () { return from; }
to function () { return to; }
constructor function PrivateRange(from, to) {
// Don't store the endpoints as properties of this object. Instead
// define accessor functions that return the endpoint values.
// These values are stored in the closure.
this.from = function() { return from; };
this.to = function() { return to; };
}
includes function (x) { return this.from() <= x && x <= this.to(); }
foreach function (f) {
for(var x=Math.ceil(this.from()), max=this.to(); x <= max; x++) f(x);
}
toString function () { return "(" + this.from() + "..." + this.to() + ")"; }
undefined
使用闭包的封装技术,要比不使用封装开销更大(占用更多内存)。
1.6.7.构造函数的重载和工厂方法
有时候我们需要类实例可以通过多种方法初始化,也就是我们需要多种构造函数,在这里我们使用了重载-overload
让它根据传入参数的不同来执行不同的初始化方法。
下面会说到两个例子,一个是Set类,另外一个是Complex类。
下面就是重载Set()构造函数的例子:
function Set() {
this.values = {}; // The properties of this object hold the set
this.n = 0; // How many values are in the set
// If passed a single array-like object, add its elements to the set
// Otherwise, add all arguments to the set
if (arguments.length == 1 && isArrayLike(arguments[0]))
this.add.apply(this, arguments[0]);
else if (arguments.length > 0)
this.add.apply(this, arguments);
}
上面这个构造函数有个缺点,我们无法创建一个Set,它只有一个唯一的成员,而且这个成员正好是一个数组。
我们只能这样解决-先创建一个空的Set,然后使用add方法把那个数组加进去。
再说说Complex类,我们希望使用极坐标来初始化一个复数。
我们采用了工厂方法来返回一个类的实例。
Complex.polar = function(r, theta) {
return new Complex(r*Math.cos(theta), r*Math.sin(theta));
};
Set类也有类似的工厂方法
Set.fromArray = function(a) {
s = new Set(); // Create a new empty set
s.add.apply(s, a); // Pass elements of array a to the add method 将数组a的成员作为参数传入add()方法
return s; // Return the new set
};
另外讲讲辅助构造函数
// An auxiliary constructor for the Set class.
//SetFromArray(a)是Set类的第二个构造函数,因为他们的prototype是一样的。
// An auxiliary constructor for the Set class.
function SetFromArray(a) {
// Initialize new object by invoking Set() as a function,
// passing the elements of a as individual arguments.
Set.apply(this, a);
}
// Set the prototype so that SetFromArray creates instances of Set
SetFromArray.prototype = Set.prototype;
var s = new SetFromArray([1,2,3]);
s instanceof Set // => true
1.7.子类
1.7.1.定义一个子类
定义子类
Example 9-11. Subclass definition utilities
// A simple function for creating simple subclasses
function defineSubclass(superclass, // Constructor of the superclass
constructor, // The constructor for the new subclass
methods, // Instance methods: copied to prototype
statics) // Class properties: copied to constructor
{
// Set up the prototype object of the subclass
constructor.prototype = inherit(superclass.prototype);
constructor.prototype.constructor = constructor;
// Copy the methods and statics as we would for a regular class
if (methods) extend(constructor.prototype, methods);
if (statics) extend(constructor, statics);
// Return the class
return constructor;
}
// We can also do this as a method of the superclass constructor
Function.prototype.extend = function(constructor, methods, statics) {
return defineSubclass(this, constructor, methods, statics);
};
1.7.2.构造函数和方法链
下面定义一个Set的子类-NonNullSet
它不允许null和undefined作为他的集合元素。从新实现一个add方法,在这个add方法里面做了过滤,过滤通过的情况再调用Set的add方法,这就是我们说的方法链。
而构造函数就直接调用了Set的constructor,这时候是作为一个普通函数调用的作用仅仅是初始化对象,
也就是我们说的构造函数链。
Example 9-13. Constructor and method chaining from subclass to superclass 在子类中调用父类的构造函数和方法
/*
* NonNullSet is a subclass of Set that does not allow null and undefined
* as members of the set.
*/
function NonNullSet() {
// Just chain to our superclass.
// Invoke the superclass constructor as an ordinary function to initialize
// the object that has been created by this constructor invocation.
Set.apply(this, arguments);
}
// Make NonNullSet a subclass of Set:
NonNullSet.prototype = inherit(Set.prototype);
NonNullSet.prototype.constructor = NonNullSet;
// To exclude null and undefined, we only have to override the add() method
NonNullSet.prototype.add = function() {
// Check for null or undefined arguments
for(var i = 0; i < arguments.length; i++)
if (arguments[i] == null)
throw new Error("Can't add null or undefined to a NonNullSet");
// Chain to the superclass to perform the actual insertion
return Set.prototype.add.apply(this, arguments);
};
过滤的概念推广一下,我们做了一个类工厂filteredSetSubclass,专门用来定义各种过滤后的集合。
我们可以定义全部是String的集合StringSet,我们可以定义一个不允许函数作为元素的非空集合MySet。
Example 9-14. A class factory and method chaining 类工厂和方法链
/*
* This function returns a subclass of specified Set class and overrides
* the add() method of that class to apply the specified filter.
*/
function filteredSetSubclass(superclass, filter) {
var constructor = function() { // The subclass constructor
superclass.apply(this, arguments); // Chains to the superclass
};
var proto = constructor.prototype = inherit(superclass.prototype);
proto.constructor = constructor;
proto.add = function() {
// Apply the filter to all arguments before adding any
for(var i = 0; i < arguments.length; i++) {
var v = arguments[i];
if (!filter(v)) throw("value " + v + " rejected by filter");
}
// Chain to our superclass add implementation
superclass.prototype.add.apply(this, arguments);
};
return constructor;
}
//下面使用刚才的类工厂定义两个子类
<pre name="code" class="html">// Define a set class that holds strings only
var StringSet = filteredSetSubclass(Set,
function(x) {return typeof x==="string";});
// Define a set class that does not allow null, undefined or functions
var MySet = filteredSetSubclass(NonNullSet,
function(x) {return typeof x !== "function";});
1.7.3.组合vs.子类
组合优于继承
我们不需要实现那么多子类,我们只需要一个过滤集合就可以了,对这个过滤集合实例化(提供一个过滤函数就可以了)
Example 9-15. Composing sets instead of subclassing them
/*
* A FilteredSet wraps a specified set object and applies a specified filter
* to values passed to its add() method. All of the other core set methods
* simply forward to the wrapped set instance.
*/
var FilteredSet = Set.extend(
function FilteredSet(set, filter) { // The constructor
this.set = set;
this.filter = filter;
},
{ // The instance methods
add: function() {
// If we have a filter, apply it
if (this.filter) {
for(var i = 0; i < arguments.length; i++) {
var v = arguments[i];
if (!this.filter(v))
throw new Error("FilteredSet: value " + v + " rejected by filter");
}
}
// Now forward the add() method to this.set.add()
this.set.add.apply(this.set, arguments);
return this;
},
// The rest of the methods just forward to this.set and do nothing else.
remove: function() {
this.set.remove.apply(this.set, arguments);
return this;
},
contains: function(v) { return this.set.contains(v); },
size: function() { return this.set.size(); },
foreach: function(f,c) { this.set.foreach(f,c); }
});
上文定义过的NotNullSet,可以这样做就创建了一个集合实例
var s = new FilteredSet(new Set(), function(x) { return x !== null; });
甚至还可以对已经过滤过的集合再过滤
var t = new FilteredSet(s, { function(x} { return !(x instanceof Set); });
这个做法比较妙,我喜欢。让我想起了Generic范型,或者是模板类。
1.7.4.类的层次结构和抽象类
这里面使用了接口的概念
在js里面是抽象类的概念
例9-16虽然很长,但是值得好好看看。
这次暂时没空,下次再补充!!!
1.8.ECMASCRIPT5中的类
ECMASCRIPT5给属性特性增加了方法支持-getter/setter,可枚举性,可写性和可配置性,而且增加了对象可扩展性的限制。
我们可以利用这些特性让类更加强壮。
1.8.1.让属性不可枚举
Example 9-17. Defining nonenumerable properties
// Wrap our code in a function so we can define variables in the function scope
(function() {
// Define objectId as a nonenumerable property inherited by all objects.
// When this property is read, the getter function is invoked.
// It has no setter, so it is read-only.
// It is nonconfigurable, so it can't be deleted.
Object.defineProperty(Object.prototype, "objectId", {
get: idGetter, // Method to get value
enumerable: false, // Nonenumerable
configurable: false // Can't delete it
}
<span style="white-space:pre"> </span>);
// This is the getter function called when objectId is read
function idGetter() { // A getter function to return the id
if (!(idprop in this)) { // If object doesn't already have an id
if (!Object.isExtensible(this)) // And if we can add a property
throw Error("Can't define id for nonextensible objects");
Object.defineProperty(this, idprop, { // Give it one now.
value: nextid++, // This is the value
writable: false, // Read-only
enumerable: false, // Nonenumerable
configurable: false // Nondeletable
}
);
}
return this[idprop]; // Now return the existing or new value
};
// These variables are used by idGetter() and are private to this function
var idprop = "|**objectId**|"; // Assume this property isn't in use
var nextid = 1; // Start assigning ids at this #
}()); // Invoke the wrapper function to run the code right away 立即执行这个包装函数
1.8.2.定义不可变的类
Example 9-18. An immutable class with read-only properties and methods 不可变类-拥有只读属性和方法
// This function works with or without 'new': a constructor and factory function
//该类可以使用new或者不使用new都可以产生实例,前者使用构造函数,后者使用工厂方法
function Range(from,to) {
// These are descriptors for the read-only from and to properties.
var props = {
from: {value:from, enumerable:true, writable:false, configurable:false}, //不可写不可配置的属性
to: {value:to, enumerable:true, writable:false, configurable:false}
};
if (this instanceof Range) // If invoked as a constructor 如果使用了new就是说当作构造函数来使用了
Object.defineProperties(this, props); // Define the properties
else // Otherwise, as a factory 工厂方法
return Object.create(Range.prototype, // Create and return a new
props); // Range object with props
}
// If we add properties to the Range.prototype object in the same way,
// then we can set attributes on those properties. Since we don't specify
// enumerable, writable, or configurable, they all default to false.
//使用defineProperties,把那些实例方法加到Range.prototype里面去。属性的特性默认都是false
Object.defineProperties(Range.prototype, {
includes: {
value: function(x) { return this.from <= x && x <= this.to; }
},
foreach: {
value: function(f) {
for(var x = Math.ceil(this.from); x <= this.to; x++) f(x);
}
},
toString: {
value: function() { return "(" + this.from + "..." + this.to + ")"; }
}
}
);
我们还有一个更加直观的版本,定义了两个工具函数,freezeProps和hideProps
Example 9-19. Property descriptor utilities
// Make the named (or all) properties of o nonwritable and nonconfigurable.<pre name="code" class="html">//不可写,不可配置
function freezeProps(o) {
var props = (arguments.length == 1) // If 1 arg
? Object.getOwnPropertyNames(o) // use all props
: Array.prototype.splice.call(arguments, 1); // else named props
props.forEach(function(n) { // Make each one read-only and permanent
// Ignore nonconfigurable properties
if (!Object.getOwnPropertyDescriptor(o,n).configurable) return;
Object.defineProperty(o, n, { writable: false, configurable: false });//不可写,不可配置
});
return o; // So we can keep using it
}
// Make the named (or all) properties of o nonenumerable, if configurable. 可配置的属性设置为不可枚举的
function hideProps(o) {
var props = (arguments.length == 1) // If 1 arg
? Object.getOwnPropertyNames(o) // use all props
: Array.prototype.splice.call(arguments, 1); // else named props
props.forEach(function(n) { // Hide each one from the for/in loop
// Ignore nonconfigurable properties
if (!Object.getOwnPropertyDescriptor(o,n).configurable) return;
Object.defineProperty(o, n, { enumerable: false });
});
return o;
}
Example 9-20. A simpler immutable class 简单的不可变类,使用了上面9-19定义的工具函数
<pre name="code" class="html">ImmutableRange
<pre name="code" class="html">将属性设置为不可变的
将原型里面的实例属性设置为不可枚举的
function Range(from, to) { // Constructor for an immutable Range class this.from = from; this.to = to; freezeProps(this); // Make the properties immutable 将属性设置为不可变的}Range.prototype = hideProps({ // Define prototype with nonenumerable properties 将原型里面的实例属性设置为不可枚举的
constructor: Range, includes: function(x) { return this.from <= x && x <= this.to; }, foreach: function(f) {for(var x=Math.ceil(this.from);x<=this.to;x++) f(x);}, toString: function() { return "(" + this.from + "..." + this.to + ")"; }});//下面是做的测试,为了和其他Range定义区别开来,使用了ImmutableRange,//和例9-6的Set做对比,也可以和例9-10的PrivateRange做对比
> ImmutableRange
[Function: ImmutableRange]
> ImmutableRange.toString()
'function ImmutableRange(from, to) { // Constructor for an immutable Range class\nthis.fr
om = from;\nthis.to = to;\nfreezeProps(this); // Make the properties immutable\n
}'
> ImmutableRange.prototype
{}
> ImmutableRange.prototype.constructor
[Function: ImmutableRange]
> ImmutableRange.prototype
{}
> ImmutableRange.prototype.constructor
[Function: ImmutableRange]
>var ir=new ImmutableRange(22,555);
> for (p in ir)console.log(p,ir[p]); //prototype 里面的实例方法已经看不到了
from 22
to 555
1.8.3.封装对象状态
我们对9-10的PrivateRange还是不满意,因为在ECMASCRIPT3中,这些私有状态的存取器方法可以是被替换的,这样还是留下了漏洞额。
在ECMASCRIPT5中,可以使用getter和setter把私有状态更加健壮的封装起来,这两个方法是无法删除的。
Example 9-21. EncapedRange-A Range class with strongly encapsulated endpoints
// This version of the Range class is mutable but encapsulates its endpoint
// variables to maintain the invariant that from <= to.
function EncapedRange(from, to) {
// Verify that the invariant holds when we're created
if (from > to) throw new Error("EncapedRange: from must be <= to");
// Define the accessor methods that maintain the invariant
function getFrom() { return from; }
function getTo() { return to; }
function setFrom(f) { // Don't allow from to be set > to
if (f <= to) from = f;
else throw new Error("Range: from must be <= to");
}
function setTo(t) { // Don't allow to to be set < from
if (t >= from) to = t;
else throw new Error("Range: to must be >= from");
}
// Create enumerable, nonconfigurable properties that use the accessors
Object.defineProperties(this, {
from: {get: getFrom, set: setFrom, enumerable:true, configurable:false},
to: { get: getTo, set: setTo, enumerable:true, configurable:false }
}
);
}
// The prototype object is unchanged from previous examples-9-20
// The instance methods read from and to as if they were ordinary properties.
Range.prototype = hideProps({
constructor: Range,
includes: function(x) { return this.from <= x && x <= this.to; },
foreach: function(f) {for(var x=Math.ceil(this.from);x<=this.to;x++) f(x);},
toString: function() { return "(" + this.from + "..." + this.to + ")"; }
});
1.8.4.防止类的扩展
通常认为,通过给原型方法添加方法,可以动态的对类进行扩展,这是javascript本身的特性。
ECMASCRIPT5可以根据需要对此特性进行限制。
Object.preventExtensions()可以将对象设置为不可扩展的,也就是不能给对象添加任何属性。
Object.seal()则更进一步,它除了能阻止用户给对象添加新属性,还能将当前已有的属性设置为不可配置的,也就是不能删除这些属性了(但是这些不可配置的属性可以是可写的也可以转换为只读的)
Object.seal(Object.prototype);
javascript另外一个动态特性是可以随时替换类方法(或者称之为monkey-patching)
var original_sort_method = Array.prototype.sort;
Array.prototype.sort = function() {
var start = new Date();
original_sort_method.apply(this, arguments);
var end = new Date();
console.log("Array sort took " + (end - start) + " milliseconds.");
};
可以通过将实例方法设置为只读来防止这类修改,一种方法就是使用我们在9-19里面定义的freezeProps()工具函数。
另外一种方法就是使用Object.freeze(),freeze除了做了Object.seal()做的以外,还把所有属性设置为只读的和不可配置的。
想下面这样子做就可以了:
Object.freeze(enumeration.values);
Object.freeze(enumeration);
但是我们需要注意一点的是,像例9-17里面有一个objectid这样的内部属性,他是需要设置为某个值,所以在设置它以后才能够像上面那样调用freeze函数。
1.8.5.子类和ECMASCRIPT5
下面使用了ECMASCRIPT5的特点定义子类,StringSet继承了9-16中的AbstractWritableSet。
需要的几个地方:
1.构造函数里面使用了Object.create(null),据说这样的好处是可以直接使用in,而不需要使用hasOwnProperty,这一点需要验证一下,我还不是很明白。
2.使用了属性描述符,没有提到的几个属性特性默认值都是false,所以是不可以被子类化的(被继承)。
Example 9-22. StringSet: a set subclass using ECMAScript 5
function StringSet() {
this.set = Object.create(null); // Create object with no proto
this.n = 0;
this.add.apply(this, arguments);
}
// Note that with Object.create we can inherit from the superclass prototype
// and define methods in a single call. Since we don't specify any of the
// writable, enumerable, and configurable properties, they all default to false.
// Readonly methods makes this class trickier to subclass.
StringSet.prototype = Object.create(AbstractWritableSet.prototype, {
constructor: { value: StringSet },
contains: { value: function(x) { return x in this.set; } },
size: { value: function(x) { return this.n; } },
foreach: { value: function(f,c) { Object.keys(this.set).forEach(f,c); } },
add: {
value: function() {
for(var i = 0; i < arguments.length; i++) {
if (!(arguments[i] in this.set)) {
this.set[arguments[i]] = true;
this.n++;
}
}
return this;
}
},
remove: {
value: function() {
for(var i = 0; i < arguments.length; i++) {
if (arguments[i] in this.set) {
delete this.set[arguments[i]];
this.n--;
}
}
return this;
}
}
9.8.6.属性描述符
下面的例子集中展现了属性描述符的风采,作为一种调试的手段,也提出了一种hideProperties/freezeProperties的替代方案。
另外也融入了模块化的思想,下面一节会讲到。
Example 9-23. ECMAScript 5 properties utilities
/*
* Define a properties() method in Object.prototype that returns an
* object representing the named properties of the object on which it
* is invoked (or representing all own properties of the object, if
* invoked with no arguments). The returned object defines four useful
* methods: toString(), descriptors(), hide(), and show().
*/
(function namespace() { // Wrap everything in a private function scope
// This is the function that becomes a method of all object
function properties() {
var names; // An array of property names
if (arguments.length == 0) // All own properties of this
names = Object.getOwnPropertyNames(this);
else if (arguments.length == 1 && Array.isArray(arguments[0]))
names = arguments[0]; // Or an array of names
else // Or the names in the argument list
names = Array.prototype.splice.call(arguments, 0);
// Return a new Properties object representing the named properties
return new Properties(this, names);
}
// Make it a new nonenumerable property of Object.prototype.
// This is the only value exported from this private function scope.
Object.defineProperty(Object.prototype, "properties", {
value: properties,
enumerable: false, writable: true, configurable: true
});
// This constructor function is invoked by the properties() function above.
// The Properties class represents a set of properties of an object.
function Properties(o, names) {
this.o = o; // The object that the properties belong to
this.names = names; // The names of the properties
}
// Make the properties represented by this object nonenumerable
Properties.prototype.hide = function() {
var o = this.o, hidden = { enumerable: false };
this.names.forEach(function(n) {
if (o.hasOwnProperty(n))
Object.defineProperty(o, n, hidden);
});
return this;
};
// Make these properties read-only and nonconfigurable
Properties.prototype.freeze = function() {
var o = this.o, frozen = { writable: false, configurable: false };
this.names.forEach(function(n) {
if (o.hasOwnProperty(n))
Object.defineProperty(o, n, frozen);
});
return this;
};
// Return an object that maps names to descriptors for these properties.
// Use this to copy properties along with their attributes:
// Object.defineProperties(dest, src.properties().descriptors());
Properties.prototype.descriptors = function() {
var o = this.o, desc = {};
this.names.forEach(function(n) {
if (!o.hasOwnProperty(n)) return;
desc[n] = Object.getOwnPropertyDescriptor(o,n);
});
return desc;
};
// Return a nicely formatted list of properties, listing the
// name, value and attributes. Uses the term "permanent" to mean
// nonconfigurable, "readonly" to mean nonwritable, and "hidden"
// to mean nonenumerable. Regular enumerable, writable, configurable
// properties have no attributes listed.
Properties.prototype.toString = function() {
var o = this.o; // Used in the nested function below
var lines = this.names.map(nameToString);
return "{\n " + lines.join(",\n ") + "\n}";
function nameToString(n) {
var s = "", desc = Object.getOwnPropertyDescriptor(o, n);
if (!desc) return "nonexistent " + n + ": undefined";
if (!desc.configurable) s += "permanent ";
if ((desc.get && !desc.set) || !desc.writable) s += "readonly ";
if (!desc.enumerable) s += "hidden ";
if (desc.get || desc.set) s += "accessor " + n
else s += n + ": " + ((typeof desc.value==="function")?"function"
:desc.value);
return s;
}
};
// Finally, make the instance methods of the prototype object above
// nonenumerable, using the methods we've defined here.
Properties.prototype.properties().hide();
}()); // Invoke the enclosing function as soon as we're done defining it.
9.9. 模块
javascript本身不具有模块化的语言级别的元素,虽然imports,exports是关键字但是还没有用得上。
在某些js框架中,其实已经使用了一些方法,可以模拟模块化编程。所以要根据js框架的情况,来考虑。
下面使用了一个匿名函数(函数表达式而不是函数语句)作为命名空间。
这是javascript的一种惯用法,大家必须要属性这种用法,否则看看就晕了。
为了改善可读性,这里给匿名函数加了一个名字invocation,也可以起名namespace.
Example 9-24. A Set class in a module function
// Declare a global variable Set and assign it the return value of this function
// The open parenthesis and the function name below hint that the function
// will be invoked immediately after being defined, and that it is the function
// return value, not the function itself, that is being assigned.
// Note that this is a function expression, not a statement, so the name
// "invocation" does not create a global variable.
var Set = (function invocation() {
function Set() { // This constructor function is a local variable.
this.values = {}; // The properties of this object hold the set
this.n = 0; // How many values are in the set
this.add.apply(this, arguments); // All arguments are values to add
}
// Now define instance methods on Set.prototype.
// For brevity, code has been omitted here
Set.prototype.contains = function(value) {
// Note that we call v2s(), not the heavily prefixed Set._v2s()
return this.values.hasOwnProperty(v2s(value));
};
Set.prototype.size = function() { return this.n; };
Set.prototype.add = function() { /* ... */ };
Set.prototype.remove = function() { /* ... */ };
Set.prototype.foreach = function(f, context) { /* ... */ };
// These are helper functions and variables used by the methods above
// They're not part of the public API of the module, but they're hidden
// within this function scope so we don't have to define them as a
// property of Set or prefix them with underscores.
function v2s(val) { /* ... */ }
function objectId(o) { /* ... */ }
var nextId = 1;
// The public API for this module is the Set() constructor function.
// We need to export that function from this private namespace so that
// it can be used on the outside. In this case, we export the constructor
// by returning it. It becomes the value of the assignment expression
// on the first line above.
return Set;
}()); // Invoke the function immediately after defining it.
下面展示了如何导出封装进入模块的公用API,而且是一个模块里面有好多API的情况。
// Create a single global variable to hold all collection-related modules
var collections;
if (!collections) collections = {};
// Now define the sets module
collections.sets = (function namespace() {
// Define the various set classes here, using local variables and functions
// ... Lots of code omitted...
// Now export our API by returning a namespace object
return {
// Exported property name : local variable name
AbstractSet: AbstractSet,
NotSet: NotSet,
AbstractEnumerableSet: AbstractEnumerableSet,
SingletonSet: SingletonSet,
AbstractWritableSet: AbstractWritableSet,
ArraySet: ArraySet
};
}());
公开模块中的API,还有两种方法,就不一一列出了。