本内容是《Web前端开发之Javascript视频》的课件,请配合大师哥《Javascript》视频课程学习。
ES中只定义了少量的数据类型:null、undefined、Boolean、Number、String、Function和Object等;typeof可以获取到基本数据类型及Object类型或Function;
在实际的应用中,会期望将类作为一个具体的类型来对待,这样就可以根据对所属的类来区分它们;
对于Es所有内置对象,可以通过以下的方法来获取或自定义类名;但对于自定义的类,其依然会返回Object;因此,还必须使用其他的几种方式来检测任意对象的类型,但都不是太完美;
类属性:
对象的类属性(class attribute)是一个字符串,用以表示对象的类型信息;ES5并没有提供设置这个属性的方法,并只有一种间接的方法可以查询它,即默认的toString(),比如,打印Object类型的对象时,返回[object Object],因此,想要获得对象的类,可以调用对象的toString方法,然后提取返回字符串的第8个到第二个位置之间的字符;
但是,很多对象继承的toString()方法重写了,为了能调用正确的toString()版本,必须间接地调用Function.call()方法,如:
function classof(o){
if(o===null) return "Null";
if(o===undefined) return "Undefined";
return Object.prototype.toString.call(o).slice(8,-1);
}
console.log(classof(null));
console.log(classof(1));
console.log(classof(""));
console.log(classof(false));
console.log(classof({}));
console.log(classof([]));
console.log(classof(/./));
console.log(classof(new Date()));
console.log(classof(window));
function F(){}
console.log(classof(new F()));
classof()函数可以传入任何类型的参数;但是对自定义类型,返回的也是Object,也就是说通过类属性没办法区分自定义类型;
检测对象的类型:
instanceof运算符:
如果o继承自c.prototype,那表达式o instanceof c值为true;这里的继承可以不是直接继承,如果o所继承的对象继承自另一个对象,后一个对象继承自c.prototype,这个表达式的运算结果也是true;
构造函数是类的公共标识,但原型是唯一 的标识;尽管instanceof右操作数是构造函数,但计算过程实际上是检测了对象的继承关系,而不是检测创建对象的构造函数;
如果想检测对象的原型链上是否存在某个特定的原型对象,可以使用isPrototypeOf()方法,如:
rang.methods.isPrototypeOf(r); // rang.methods是原型对象
instanceof和isPrototpyeOf()方法的缺点时,无法通过对象来获得类名,只能检测对象是否属于指定的类名;在客户端Javascript中有一个比较严重的问题,就是在多窗口和多框架子页面的Web应用中兼容性不好;每个窗口和框架子页面都具有单独的执行上下文,每个上下文都包含独有的全局变量和一组构造函数;在两个不同框架页面中创建的两个数组继承自两个相同但相互独立的原型对象,其中一个框架页面中的数组不是另一个框架页面的Array()构造函数的实例,instanceof运算结果是false;
构造函数和类的标识:
原型对象是类的唯一标识,当且仅当两个对象继承自同一个原型对象时,它们才是属于同一个类的实例;
而初始化对象的状态的构造函数则不能作为类的标识,两个构造函数的prototype属性可能指向同一个原型对象,那么这两个构造函数创建的实例是属于同一个类的;
尽管构造函数不像原型那样基础,但构造函数是类的“外在表现”;构造函数的名字通常用做类名,比如Person()构造函数创建Person对象;然而,更根本地讲,当使用instance运算符来检测对象是否属于某个类时会用到构造函数,如 p instanceof Person,但实际上instanceof运算符并不会检查p是否是由Person()构造函数初始化而来,而会检查p是否继承Person.prototype;
constructor属性:
另一种识别对象是否属于某个类的方法是使用constructor属性,因为构造函数是类的公共标识,所以最直接的方法就是使用constructor属性,如:
function typeAndValue(x) {
if(x==null) return ""; // Null和undefined没有构造函数
switch(x.constructor){
case Number: return "Number: " + x;
case String: return "String: '" + x + "'";
case Date: return "Date: " + x;
case RegExp: return "RegExp: " + x;
case Complex: return "Complex: " + x;
}
}
使用constructor属性检测对象属于某个类的技术的不足之处和instanceof一样;在多个执行上下文的场景中它是无法正常工作的;
另外,在Javascript中也并非所有的对象都包含constructor属性;在每个新创建的函数原型上默认会有constructor属性,但我们经常会忽略原型上的constructor属性,比如前面的示例代码中所定义的两个类,它们的实例都没有constructor属性;
构造函数的名称:
另一种检测的可能的方式 是使用构造函数的名字而不是构造函数本身作为类标识符;
两个不同窗口的Array构造函数是不相等的,但它们的名字是一样的;在一些Javascript的实现中为函数对象提供了一个非标准的属性name,用来表示函数的名称;对于那些没有name属性的Javascript实现来说,可以将函数转换为字符串,再从中提取出函数名,比如,下面的函数的getName()方法,就是使用这种方式取得函数名;如:
// 可以判断值的类型的type()函数
function type(o) {
var t, c, n; // type,class,name
if(o === null) return "null"; // 处理null值
if(o !== o) return "nan"; // NaN和它自身不相等
// 如果typeof的值不是object,则使用这个值,即可以识别出原始值和函数
if((t = typeof o) !== "object") return t;
// 返回对象的类型,除非值为Object,可以识别出大多数的内置对象
if((c = classof(o)) !== "object") return c;
// 如果对象构造函数的名字存在的话,则返回它
if(o.constructor && typeof o.constructor === "function" &&
(n = o.constructor.getName())) return n;
// 其他的类型都无法差断,一律返回Object
return "Object";
}
// 返回对象的类
function classof(o) {
return Object.prototype.toString.call(o).slice(8,-1);
}
// 返回函数的名字(可能是空字符串),不是函数的话,返回null
Function.prototype.getName = function (){
if("name" in this) return this.name;
return this.name = this.toString().match(/function\s*([^(]*)\(/)[1];
}
鸭式辩型:
上面所描述的检测对象的类的各种技术多少都会有问题,至少在客户端Javascript中是如此;解决办法就是规避掉这些问题:不要关注“对象的类是什么”,而是关注“对象能做什么”;这种思考问题的方式 在Python和Ruby中非常普通,称为“鸭式辩型”;
鸭式辩型,就是说检测对象是否实现了一个或多个方法;一个强类型的函数需要的参数必须是某种类型,而“鸭式辩型”,只要对象包含某些方法,就可以作为参数传入;
鸭式辩型在使用时,需要对输入对象进行检查,但不是检查它们的类,而是用适当的名字来检查它们所实现的方法;
示例:quacks()函数用以检查一个对象(第一个参数)是否实现了剩下的参数所表示的方法:
// 利用鸭式辩型实现的函数
// 如果o实现了除第一个参数之外的参数所表示的方法,则返回true
function quacks(o /*, ...*/) {
for(var i=1; i<arguments.length; i++){ // 遍历o之后所有参数
var arg = arguments[i];
switch(typeof arg){
case 'string':
if(typeof o[arg] !== "function")
return false;
continue;
case 'function':
// 如果实参是函数,则使用它的原型
arg = arg.prototype; // 进行下一个case
case 'object':
for(var m in arg){ // 遍历对象的每个属性
// 跳过不是方法的属性
if(typeof arg[m] !== "function") continue;
if(typeof o[m] !== "function") return false;
}
}
}
// 如果程序能执行到这里,说明o实现了所有的方法
return true;
}
集合类:
集合(set)是一种数据结构,用以表示非重复值的无序集合;集合的基础方法包括添加值、检测值是否在集合中,这种集合需要一种通用的实现,以保证操作效率;
一个更加通用的Set类,它实现了从Javascript值到唯一字符串的映射,将字符串用做属性名;对象和函数具备可靠的唯一字符串表示;因此集合类必须给集合中的每个对象或函数定义一个唯一的属性标识:
// Set.js 值的任意集合
function Set(){
this.values = {}; //集合数据保存在对象的属性里
this.n = 0; // 集合中值的个数
this.add.apply(this, arguments); // 把所有参数都添加进这个集合
}
// 把所有参数都添加进这个集合
Set.prototype.add = function(){
for(var i=0; i<arguments.length; i++){
var val = arguments[i];
var str = Set._v2s(val); // 把它转换为字符串
if(!this.values.hasOwnProperty(str)){ // 如果不在集合里
this.values[str] = val;
this.n++;
}
}
return this; // 支持链式调用
};
// 从集合删除元素,这些元素由参数指定
Set.prototype.remove = function(){
for(var i=0;i<arguments.length; i++){
var str = Set._v2s(arguments[i]);
if(this.values.hasOwnProperty(str)){
delete this.values[str];
this.n--;
}
}
};
// 如果集合包含该值,返回true,反之返回false
Set.prototype.contains = function(value){
return this.values.hasOwnProperty(Set._v2s(value));
};
// 返回集合的大小
Set.prototype.size = function(){
return this.n;
};
// 遍历集合中的所有元素,在指定的上下文中调用f
Set.prototype.foreach = function(f, context){
for(var s in this.values){
if(this.values.hasOwnProperty(s)) // 忽略继承的属性
f.call(context, this.values[s]);
}
};
// 内部函数,用以将任意Javascript值和唯一的字符串对应起来
Set._v2s = function(val){
switch(val){
case undefined: return 'u'; // 特殊的原始值
case null: return 'n'; // 只有一个字母代码
case true: return 't';
case false: return 'f';
default: switch(typeof val){
case 'number': return '#' + val; // 数字带#前缀
case 'string': return '"' + val; // 字符串带"前缀
default: return '@' + objectId(val); // 对象和函数带有@
}
}
function objectId(o){
var prop = "|**objectid**|"; // 私有属性,用于存放id
if(!o.hasOwnProperty(prop)){ // 如果对象没有id
o[prop] = Set._v2s.next++; // 将下一个值赋给它
}
return o[prop];
}
};
Set._v2s.next = 100; // 设置初始id的值
// 应用
var persons = new Set("a","b");
persons.add("c",1,null,{name:"wangwei"});
console.log(persons);
console.log(persons.contains("b"));
console.log(persons.size());
function show(v){
console.log("元素:" + v);
}
persons.foreach(show,window);
枚举类型-示例:
枚举类型是一种类型,它是值的有限集合,如果值定义为这个类型则该值是可列出(可枚举)的;
以下的示例包含一个单独函数enumeration(),但它不是构造函数,它没有定义一个名叫enumeration的类,相反,它是一个工厂方法,每次调用它创建一个新的类,如:
// 使用4个值创建新的Coin类
var Coin = enumeration({Penny:1, Nickel:5, Dime:10, Quarter:25});
var c = Coin.Dime; // 这是新类的实例
console.log(c instanceof Coin); // true
console.log(c.constructor == Coin); // true
console.log(Coin.Quarter + 3 * Coin.Nickel); // 40,将值转换为数字
Coin.Dime == 10; // true, 更多转换为数字的例子
Coin.Dime > Coin.Nickel; // true
String(Coin.Dime) + ":" + Coin.Dime;
// 枚举类型
// 实参对象表示类的每个实例的名/值
// 返回一个构造函数,它标识这个新类
// 注:这个构造函数也会抛出异常,不能使用它来创建该类型的新实例
// 返回的构造函数包含名/值对的映射表
// 包括由值组成的数组,以及一个foreach()迭代器
function enumeration(namesToValues){
// 这个虚拟的构造函数是返回值
var enumeration = function(){throw "Can't Instantiate Enumerations";};
// 枚举值继承自这个对象
var proto = enumeration.prototype = {
constructor: enumeration, // 标识类型
toString: function(){ return this.name;}, // 返回名字
valueOf: function(){ return this.value;}, // 返回值
toJSON: function(){ return this.name;} // 转换为JSON
};
// 用以存放枚举对象的数组
enumeration.values = [];
// 创建新类型的实例
for(name in namesToValues){
var e = Object.create(proto); // 创建一个代表它的对象
e.name = name; // 给它一个名字
e.value = namesToValues[name]; // 给它一个值
enumeration[name] = e; // 将它设置为构造函数的属性
enumeration.values.push(e);
}
// 一个类方法,用来对类的实例进行迭代
enumeration.foreach = function(f,c){
for(var i=0; i<this.values.length; i++)
f.call(c, this.values[i]);
};
// 返回标识这个新类型的构造函数
return enumeration;
}
用以上定义的枚举类型实现一副扑克牌的类:
// 定义一个表示玩牌的类
function Card(suit, rank){
this.suit = suit; // 每张牌都有花色
this.rank = rank; // 点数
}
// 使用枚举类型定义花色和点数
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, Quee:12, King:13, Ace:14});
// 定义用以描述牌面的文本
Card.prototype.toString = function(){
return this.rank.toString() + " of " + this.suit.toString();
};
// 比较扑克牌中两张牌的大小
Card.prototype.compareTo = function(that){
if(this.rank < that.rank) return -1;
if(this.rank > that.rank) return 1;
return 0;
};
// 以扑克牌的玩法规则对牌进行排序的函数
Card.orderByRank = function(a, b){ return a.compareTo(b);};
// 以桥牌的玩法规则对牌进行排序的函数
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;
};
// 定义用以表示一副标准扑克牌的类
function Deck(){
var cards = this.cards = []; // 一副牌就是由牌组成的数组
Card.Suit.foreach(function(s){ // 初始化这个数组
Card.Rank.foreach(function(r){
cards.push(new Card(s, r));
});
})
}
// 洗牌的方法:重新洗牌并返回洗好的牌
Deck.prototype.shuffle = function(){
// 遍历数组中的每个元素,随机找出牌面最小的元素,并与之(当前遍历的元素)交换
var deck = this.cards, len = deck.length;
for(var i = len-1; i>0; i--){
var r = Math.floor(Math.random()*(i+1)), temp; // 随机数
temp = deck[i], deck[i] = deck[r], deck[r] = temp; // 交换
}
return this;
}
// 发牌的方法:返回牌的数组
Deck.prototype.deal = function(n){
if(this.cards.lenght < n) throw "Out of cards";
return this.cards.splice(this.cards.length - n, n);
};
// 创建一副新扑克牌,洗牌并发牌
var deck = (new Deck()).shuffle();
var hand = deck.deal(13).sort(Card.orderBySuit);
console.log(deck);
console.log(hand);
标准转换方法:
对象类型转换所用到的方法,其中有一些在进行转换时由Javascript解释器自动调用;
不需要为定义的每个类都实现这些方法,但这些方法的确非常重要,如果没有为自定义的类实现这些方法,也应当是有意为之,而不应该忽略掉它。
toString()方法;这个方法的作用是返回一个可以表示这个对象的字符串;在希望使用字符串的地方用到对象的话,Javascript会自动调用这个方法;如果没有实现这个方法,类会默认从Object.prototype中继承toString()方法,这个方法的运算结果是“[object Object]”,这个字符串用处不大;toString()方法应当返回一个可读的字符串,这样最终用户才能将这个输出值利用起来,然而有时候并不一定非要如此,不管怎样,可以返回可读字符串的toString()方法也会让后续的工作更加轻松;
toLocaleString()和toString极为类似:toLocaleString()是以本地敏感性的方式来将对象转换为字符串;默认情况下,对象所继承的toLocaleString()方法只是简单地调用toString()方法;有一些内置类型包含有用的toLocaleString()方法用以返回本地化相关的字符;如果需要为对象以字符的转换定义toString()方法,那么同样需要定义toLocaleString()方法用以处理本地化的对象到字符串的转换;
valueOf()方法,它用来将对象转换为原始值;比如,当数学运算符(除了“+”)和关系运算符作用于数字文本表示的对象时,会自动调用 valueOf()方法;大多数对象都没有合适的原始值来表示它们,也没有定义这个方法;
toJSON()方法,这个方法是由JSON.stringify()自动调用的;JSON格式用于序列化良好的数据结构,而且可以处理Javascript原始值、数组长和纯对象;它和类无关,当对一个对象执行序列化操作时,它会忽略对象的原型和构造函数;比如将Range对象或Complex对象作为参数传入JSON.stringify(),将会返回诸如{“from”:1, “to”:3}或{…}这种字符串;如果将这些字符串传入JSON.parse(),则会得到一个和Range对象或Complex对象具有相同属性的纯对象,但这个对象不会包含从Range和Complex继承来的方法;
这种序列化操作非常适用于诸如Range类和Complex这种类,但对于其他一些类则必须自定义toJSON方法来定制个性化的序列化格式;如果一个对象有toJSON方法,JSON.stringify()并不会对传入的对象做序列化操作,而会调用 toJSON()来执行序列化操作;比如,Date对象的toJSON()方法可以返回一个表示日期的字符;
对于一个集合,最接近JSON的表示方法就是数组;
// 将这些方法添加到Set类的原型对象中
extend(Set.prototype,{
// 将集合转换为字符
toString: function(){
var s = "{", i=0;
this.foreach(function(v){s += ((i++>0) ? ", " : "") + v;});
return s + "}";
},
// 类似toString,但是对于所有的值都将调用 toLocaleString()
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(); // 其他情况
});
return s + "}";
},
// 将集合转换为值数组
toArray: function(){
var a = [];
this.foreach(function(v){ a.push(v);});
}
});
// 对于要从JSON转换为字符串的集合都被当做数组来对待
Set.prototype.toJSON = Set.prototype.toArray;
比较方法:
Javascript的相等运算符比较对象时,比较的是引用而不是值;也就是说,给定两个对象引用,如果要看它们是否指向同一个对象,不是检查这两个对象是否具有相同的属性名和相同的属性值 ,而是直接比较这两个单独的对象是否相等,或者比较它们的顺序;
如果定义一个类,并且希望比较类的实例,应该定义合适的方法来执行比较操作;
Java语言有很多用于对象比较的方法,Javascript可以模拟这些方法;为了能让自定义类的实例具备比较的功能,定义一个名为equals()实例方法;这个方法只能接收一个实参,如果这个实参和调用此方法的对象相等的话则返回true;当然,这个相等的含义是根据类的上下文来决定的;
对于简单的类,可以通过简单地比较它们的constructor属性来确保两个对象是相同类型,然后比较两个对象的实例属性以保证它们的值相等,如:
// 重写它的constructor属性
Range.prototype.constructor = Range;
// 一个Range对象和其他不是Range的对象均不相等
// 当且仅当两个范围的端点相等,它们才相等
Range.prototype.equals = function(that){
if(that == null) return false;
if(that.constructor != Range) return false; // 处理非Range对象
return this.from == that.from && this.to == that.to;
};
给Set类定义equals()方法,如:
Set.prototype.equals = function(that){
if(this === that) return true; // 一些闪要情况的快捷处理
// 如果that对象不是一个集合,它和this不相等
// 用到了instanceof,使得这个方法可以用于Set的任何子类
// 如果希望采用鸭式辩型的方法,可以降低检查的严格程序
// 或者可以通过this.constructor == that.constructor来加强检查的严格程序
// 注,null和undefined两个值是无法用于instanceof运算的
if(!(that instanceof Set)) return false;
// 如果两个集合的大小不一样,则它们不相等
if(this.size() != that.size()) return false;
// 现在检查两个集合中的元素是否完全一样
// 如果两个集合不相等,则通过抛出异常来终止foreach循环
try{
this.forEach(function(v){ if(!that.contains(v)) throw false;});
return true;
}catch(x){
if(x===false) return false; // 如果集合中有元素在另外一个集合中不存在
throw x; // 重新抛出异常
}
}
如果将对象用于Javascript的关系比较运算符,比如:<和<=,Javascript会首先调用对象的valueOf方法,如果这个方法返回一个原始值,则直接比较原始值;但大多数类并没有valueOf()方法,为了按照显式定义的规则来比较这些类型的对象,可以定义一个compareTo()的方法;如:
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; // 比较下边界
if(diff == 0) diff = this.to - that.to; // 如果相等,再比较上边界
return diff;
}
给类定义了compareTo()方法后,可以对类的实例组成的数组进行排序了;Array.sort()方法可以接收一个可选的参数,这个参数是一个函数,用来比较两个值的大小,这个函数返回值的约定和compareTo()方法保持一致,如:
rangs.sort(function(a,b){return a.compareTo(b);});
排序运算非常重要,如果已经为类定义了实例方法compareto(),还应当参照这个方法定义一个可传入这两个参数的比较函数;如:
Range.byLowerBound = function(a,b){
return a.compareTo(b);
};
ranges.sort(Range.byLowerBound);
方法借用:
多个类中的方法可以共用同一个单独的函数,比如,Array类通常定义了一些内置方法,如果定义了一个类,它的实例是类数组的对象,则可以从Array.prototype中将函数复制至所定义的类的原型对象中;
如果以经典的面向对象语言的视角来看Javascript的话,把一个类的方法用到其他的类中的做法也称做:多重继承,然而,Javascript并不是经典的面向对象语言,所以将这种方法重用称为“方法借用“;
不仅Array的方法可以借用,还可以自定义泛型方法,如:
var generic = {
// 返回一个字符串,这个字符串包含构造函数的名字
// 以及所有非继承来的、非函数属性的名字和值
toString: function(){
var s = '[';
if(this.constructor && this.constructor.name)
s += this.constructor.name + ": ";
// 枚举所有非继承且非函数的属性
var n = 0;
for(var name in this){
if(!this.hasOwnProperty(name)) continue; // 跳过继承的属性
var value = this[name];
if(typeof value === "function") continue // 跳过方法
if(n++) s += ", ";
s += name + "=" + value;
}
return s + ']';
},
// 这种方法适合于那些实例属性是原始值的情况
// 这里还处理一种特殊的情况,就是忽略由Set类添加的特殊属性
equals: function(that){
if(that == null) return false;
if(this.constructor !== that.constructor) return false;
for(var name in this){
if(name === "|**objectid**|") continue; // 跳过特殊属性
if(!this.hasOwnProperty(name)) continue; // 跳过继承来的属性
if(this[name] !== that[name]) return false; // 比较是否相等
}
return true; // 如果所有属性都匹配,两个对象相等
}
};
Range.prototype.equals = generic.equals;
私有状态:
在经典的面向对象编程中,经常需要将对象的某个状态封装或隐藏在对象内,只有通过对象的方法才能访问这些状态,对外只暴露一些重要的状态变量可以直接读写;为了实现这个目的,类似Java的编程语言允许声明类的“私有“实例字段,这些私有实例字段只能被类的实例方法访问,且在类的外部是不可见的。
可以通过将变量(或参数)闭包在一个构造函数内来模拟实现私有实例字段,调用构造函数会创建一个实例;
// 对Range类的读取端点方法的简单封装
function Range(from, to){
// 不要将端点保存为对象的属性,相反定义存取器函数来返回端点的值
// 这些值都保存在闭包中
this.from = function(){return from;};
this.to = function(){return to;}
}
// 原型上的方法无法直接操作端点,它们必须调用存取器方法
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() + ")";}
}
这种封装技术造成了更多系统开销,使用闭包来封装类的状态的类一定会比不使用封装的状态变量的等价类运行速度更慢,并占用更多内存。
构造函数的重载和工厂方法:
有时候,对象的初始化需要多种方式,可以通过重载这个构造函数让它根据传入参数的不同来执行不同的初始化方法,如,重载Set构造函数:
function Set(){
this.values = {};
this.n = 0;
// 如果传入一个类数组的对象,将这个元素添至集合中
// 否则,将所有的参数都添加至集合中
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.fromArray = function(a){
s = new Set(); // 创建一个空集合
s.add.apply(s,a); // 将数组a的成员作为参数传入add()方法
return s;
}
在ES中是可以定义多个构造函数继承自一个原型对象的,由这些构造函数的任意一个所创建的对象都属于同一类型;:
// Set类的一个辅助构造函数
function SetFromArray(a){
// 通过以函数的形式调用Set()来初始化这个新对象
// 将a的元素作为参数传入
Set.apply(this, a);
}
// 设置原型,以便SetFromArray能创建Set的实例
SetFromArray.prototype = Set.prototype;
var s = new SetFromArray([1,2,3]);
console.log(s instanceof Set); // true