JS和jquery的几个令人迷惑的问题之五-类和模块

来源:互联网 发布:触摸屏查询软件制作 编辑:程序博客网 时间:2024/04/29 13:20

这是我的一点点感悟:

感悟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 objectr.includes(2); // => true: 2 is in the ranger.foreach(console.log); // Prints 1 2 3console.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 objectvar r = new Range(1,3); // Create a range objectr.includes(2); // => true: 2 is in the ranger.foreach(console.log); // Prints 1 2 3console.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 = /^\{([^,]+),([^}]+)\}$/;//测试上面定义的类Complexvar c = new Complex(2,3); // Create a new object with the constructorvar 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 fieldComplex.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() functionfunction 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 Rangetrue> 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)12345undefined> console.log(r1){ from: 1, to: 5 }undefined> r1 instanceof rangefalse> r1 instanceof Rangefalse> 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 nonfunctionsFunction.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 valuesfunction 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);valuesnaddremovecontainssizeforeachundefined> for (p in o) console.log(p,o[p]);values { '#1': 1, '#2': 2, '#4': 4, '#55': 55 }n 4add function () {for(var i = 0; i < arguments.length; i++) { // For each argumentvar val = arguments[i]; // The value to add to the setvar str = Set._v2s(val); // Transform it to a stringif (!this.values.hasOwnProperty(str)) { // If not already in the setthis.values[str] = val; // Map string to valuethis.n++; // Increase set size}}return this; // Support chained method calls}remove function () {for(var i = 0; i < arguments.length; i++) { // For each argumentvar str = Set._v2s(arguments[i]); // Map to a stringif (this.values.hasOwnProperty(str)) { // If it is in the setdelete this.values[str]; // Delete itthis.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 setif (this.values.hasOwnProperty(s)) // Ignore inherited propertiesf.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 functionfunction 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 classc instanceof Coin // => true: instanceof worksc.constructor == Coin // => true: constructor property worksCoin.Quarter + 3*Coin.Nickel // => 40: values convert to numbersCoin.Dime == 10 // => true: more conversion to numbersCoin.Dime > Coin.Nickel // => true: relational operators workString(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 cardfunction 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 valuesCard.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 cardCard.prototype.toString = function() {  return this.rank.toString() + " of " + this.suit.toString();};// Compare the value of two cards as you would in pokerCard.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 pokerCard.orderByRank = function(a,b) { return a.compareTo(b); };// A function for ordering cards as you would in bridgeCard.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 cardsfunction 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 cardsDeck.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 handvar 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 borrowingvar 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类定义,我们把这个使用了私有状态存取器的类叫做PrivateRangefunction 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);fromtoconstructorincludesforeachtoStringundefined> 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 SetSetFromArray.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 subclassesfunction 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 constructorFunction.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() methodNonNullSet.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 onlyvar StringSet = filteredSetSubclass(Set,                                    function(x) {return typeof x==="string";});// Define a set class that does not allow null, undefined or functionsvar 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里面去。属性的特性默认都是falseObject.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 5function 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 modulesvar collections;if (!collections) collections = {};// Now define the sets modulecollections.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,还有两种方法,就不一一列出了。


0 0
原创粉丝点击