title: JavaScript学习记录三
toc: true
date: 2018-09-14 23:51:22html
——《JavaScript高级程序设计(第2版)》学习笔记设计模式
要多查阅MDN Web 文档数组
工厂模式是软件工程领域广为人知的一种设计模式,这种模式抽象了建立具体对象的过程。浏览器
用函数来封装以特定接口建立对象的细节:闭包
function createPerson(name, age, job) { var o = new Object; o.name = name; o.age = age; o.jpb = job; o.sayName = function() { alert(this.name); }; return o; } var person1 = createPerson("Nicholas", 29, "Software Engineer"); var person2 = createPerson("Greg", 27, "Doctor"); person1.sayName(); // "Nicholas" person2.sayName(); // "Greg"
工厂模式虽然解决了建立多个类似对的问题,却没有解决对象识别的问题。app
咱们能够建立自定义的构造函数,从而定义自定义对象类型的属性和方法。函数
function Person(name, age, job) { this.name = name; this.age = age; this.jpb = job; this.sayName = function() { alert(this.name); }; } var person1 = new Person("Nicholas", 29, "Software Engineer"); var person2 = new Person("Greg", 27, "Doctor"); person1.sayName(); // "Nicholas" person2.sayName(); // "Greg"
构造函数模式与工厂模式的区别:学习
没有显示地建立对象(new Object())this
函数名首字母大写.net
要建立Person的新实例,必须使用new操做符。以这种方式调用构造函数实际上会经历4个步骤:
这样经过构造函数模式建立的两个对象都有一个constructor(构造函数)属性,该属性指向Person:
person1.constructor == Person; // true person1 instanceof Person; // true person1 instanceof Object; // true, 由于全部对象均继承自Object
建立自定义的构造函数意味着未来能够将它的实例标识为一种特定的类型,这正是构造函数模式优于工厂模式的地方。
前边例子中的Person()函数能够经过下边任何一种方式来调用:
// 当作构造函数使用 var person = new Person("Nicholas", 29, "Software Engineer"); person.sayName(); // 做为普通函数调用 Person("Greg", 27, "Doctor"); // 添加到window window.sayName(); // "Greg" //在另外一个对象的做用域中调用 var o = new Obeject(); Person.call(o, "Kristen", 25, "Nurse"); o.sayName(); // "Kristen"
使用构造函数的主要问题,是每一个方法都要在每一个实例上从新建立一遍,这是没有必要的,所以Person()能够像下边这样定义:
function Person(name, age, job) { this.name = name; this.age = age; this.jpb = job; this.sayName = sayName; } function sayName() { alert(this.name); }
可是这样的话,在全局做用域中定义的函数(sayName())只能被 某个对象调用,这让全局做用域有点名存实亡,并且若是对象须要定义不少方法,那么就要定义不少个全局函数,这样咱们自定义的引用类型就毫无封装性可言。
可是这些问题能够经过使用原型模式解决。
关于prototype能够先看这一篇。
而后看下边这个例子:
function Person() {} Person.prototype.name = "Nicolas"; Person.prototype.age = 29; Person.prototype.job = "Software Engineer"; Person.prototype.sayName = function(){ alert(this.name); }; var person1 = new Person(); person1.sayName(); // "Nicolas" var person2 = new Person(); person2.sayName(); // "Nicolas" person1.sayName == person2.sayName; // true
在原型模式下,对象调用这些属性和方法时,其实是调用prototype的属性和方法。
默认状况下,全部prototype属性都会自动得到一个constructor(构造函数)属性,这个属性包含一个指向prototype所在函数的指针。
若是person1的__proto
指向Person的prototype
,则
Person.prototype.isPrototypeOf(person); // true
当为对象实例添加一个属性时,这个属性就会屏蔽源性对象中保存的同名属性,但不会修改那个属性。
若是将为对象实例添加的这个属性设为null,也只会在实例中设置这个属性,而不会恢复其指向原型的链接。
要想从新访问原型中的属性,可使用delete操做符彻底删除实例属性,
使用hasOwnProperty()能够检测一个属性是否存在于实例中(这个方法是从Object继承来的),若是是原型属性则返回false:
function Person() {} Person.prototype.name = "Nicolas"; Person.prototype.age = 29; Person.prototype.job = "Software Engineer"; Person.prototype.sayName = function(){ alert(this.name); }; var person1 = new Person(); var person2 = new Person(); person1.hasOwnProperty("name"); // false person1.name = "Greg"; person1.name; // "Greg"————来自实例 person2.name; // "Nicolas"————来自原型 person1.hasOwnProperty("name"); // true person2.hasOwnProperty("name"); // false delete person1.name; person1.name; // "Nicolas"————来自原型 person1.hasOwnProperty("name"); // false
in操做符会在经过对象可以访问给定属性时返回true,不管该属性存在于实例中仍是原型中。所以对于上面的例子,在person1和person2声明后,不管什么时候调用"name" in person1
或"name" in person2
都会获得true。
所以,在hasOwnPrototype()返回false而使用in操做符返回true时,就说明这个属性是原型属性。
in操做符还能够经过for-in循环使用,返回的是全部能经过对象访问的、可枚举的(enumerated)属性和方法。
原型中不可枚举的属性和方法(即设置了[[DontEnum]]标记的属性和方法)有hasOwnProperty()、propertyIsEnumerable()、toLocalString()、toString()和valueOf(),有的浏览器也为constructor和prototype打上标记,
可是当咱们在实例中添加这些属性和方法从而屏蔽了原型中的这些属性和方法时,那么这些属性和方法就会被认为是可枚举的(IE中除外):
var o = { toString: function() { return "My Object"; } }; for (var prop in o) { if (prop == "toString") { alert("Found toString"); // 在IE中不会显示,其余浏览器显示 } }
每添加一个属性和方法就要敲一遍Person.prototype是没必要要的,同事也为了从视觉上更好地封装原型的功能,更常见的作法是用一个包含全部属性和方法的对象字面量来重写整个原型对象:
function Person() {} Person.prototype = { /* 重写prototype会致使其constructor等于Object, * 若constructor的值很重要,能够给constructor设置回适当的值 */ constructor: Person, name: "Nicholas", age: 29, job: "Software Engineer", sayName: function(){ alert(this.name); } }; var person = new Person(); person.constructor == Person; // 如果添加了上边constructor那一句则为true
因为在原型中查找值的过程是一次搜索,所以对原型对象的修改都可以当即从实例中反映出来,
可是若是像上边的例子同样重写了原型,在重写原型以前声明的实例的__proto__
指向的还是最初的原型:
function Person() {} var person = new Person(); Person.prototype.sayHi = function() { alert("hi"); }; person.sayHi(); // "hi",没有问题 Person.prototype = { constructor: Person, name: "Nicholas", age: 29, job: "Software Engineer", sayName: function(){ alert(this.name); } }; person.sayHi(); // "hi",没有问题 person.sayName(); //error
全部原生的引用类型,都是采用原型模式建立的。所以咱们亦能够对原生引用类型的prototype添加属性或方法。
以String为例:
String.prototype.startsWith = function(text) { return this.indexOf(text) == 0; }; var msg = "Hello World!"; msg.startsWith("Hello"); // true
可是不建议在产品化的程序中修改原生对象的原型。
若是一个原型的属性包含引用类型值时,实例对该属性进行操做时,实际上修改的就是原型中的属性(引用类型对象名能够看作指针),所以当其余实例访问该属性时,获得的就是这个实例修改后的值:
function Person() {} Person.prototype = { constructor: Person, name: "Nicholas", age: 29, job: "Software Engineer", friends: ["Shelby", "Court"], // 属性值为引用类型 sayName: function(){ alert(this.name); } }; var person1 = new Person(); var person2 = new Person(); person1.friends.push("Van"); person1.friends; // ["Shelby", "Court", "Van"] person2.friends; // ["Shelby", "Court", "Van"] person1.friends == person2.friends; // true
使用构造函数模式定义实例属性,原型模式定义方法和共享的属性,
这样每一个实例都会有自已的一份实例属性的副本,又共享着对方法的引用,最大限度地节省了内存,还能够向构造函数传递参数:
function Person(name, age, job) { this.name = name; this.age = age; this.job = job; this.friends = ["Shelby", "Court"]; } Person.prototype = { constructor: Person, sayName: function(){ alert(this.name); } }; var person1 = new Person("Nicholas", 29, "Software Engineer"); var person2 = new Person("Greg", 27, "Doctor"); person1.friends.push("Van"); person1.friends; // ["Shelby", "Court", "Van"] person2.friends; // ["Shelby", "Court"] person1.friends == person2.friends; // false person1.sayName == person2.sayName; // true
这种混合使用的模式是ECMAScript中使用最普遍、认同度最高的自定义类型的方法。能够说是一种默认模式。
这种模式把全部信息都封装在了构造函数中,并在构造函数中经过检查某个应该存在的方法是否有效,来决定是否须要初始化模型:
function Person(name, age, job) { // 属性 this.name = name; this.age = age; this.job = job; // 方法 // 只有在sayName()方法不存在时才将其添加到原型中 // 即只有在初次调用构造函数时才会执行下面的代码 // if语句只须要判断一个方法(例如sayName)是否存在 if (typeof this.sayName != "function") { Person.prototype = { constructor: Person, sayName: function() { alert(this.name); }, sayHi: function() { alert("hi"); } }; } }
寄生构造函数模式和稳妥构造函数模式,寄生构造模式没有什么意义这里就再也不赘述,稳妥构造函数模式至关于为引用类型添加了private属性,有兴趣能够自行搜索。
在ECMAScript中没法实现接口继承(与函数没法重载的理由相同,ECMAScript中的函数没有签名),
可是能够利用原型链实现实现继承。
除了这一篇讲到的,还应注意:
instanceof
和isPrototypeOf()
又叫伪造继承或经典继承。
在子类型构造函数获得内部利用调用超类型的构造函数,还能够传递参数。
function SuperType(name) { this.name = name; } function SubType() { // 继承了SuperType,同时还传递了参数 SuperType.call(this, "Nicholas"); // 实例属性 this.age = 29; } var instance = new SubType(); instance.name; // "Nicholas" instance.age; // 29
可是若是方法都在构造函数中定义,函数复用就无从谈起了。
combination inheritance,伪经典继承,组合使用原型链和借用构造函数。
使用原型链实现原型属性和方法的继承,经过借用构造函数实现实例属性的继承,
这样既能够实现函数复用,又能保证每一个实例都有它本身的属性。
同时,instanceof
和isPrototypeOf
也能识别基于组合继承建立的对象。
function SuperType(name) { this.name = name; this.colors = ["red", "green", "blue"]; } SuperType.prototype.sayName = function() { alert(this.name); }; function SubType(name, age) { // 继承属性 SuperType.call(this, name); this.age = age; } // 继承方法 SubType.prototype = new SuperType(); SubType.prototype.sayAge = function(){ alert(this.age); }; var instance1 = new SubType("Nicholas", 29); instance1.colors.push("black"); instance1.colors; // ["red", "green", "blue", "black"] instance1.sayName(); // "Nicholas" instance1.sayAge(); // 29 var instance2 = new SubType("Greg", 27); instance1.colors; // ["red", "green", "blue] instance1.sayName(); // "Greg" instance1.sayAge(); // 27
组合继承融合了前二者的优势,所以成为JavaScript中最经常使用的继承模式。
主要用于只是想让一个对象与另外一个对象保持相似,没有必要兴师动众地建立构造函数。
function object(o) { function F() {} F.prototype = o; return new F(); }
这样子其实是object()函数对传入的对象执行了一次浅复制:
var person = { name: "Nicholas", friends: ["Shelby", "Court", "Van"]; }; var anotherPerson = object(person); anotherPerson.name = "Greg"; anotherPerson.friends.push("Rob"); person.friends; // ["Shelby", "Court", "Van", "Rob"] person.name; // "Nicholas" anotherPerson.name; // "Greg"
寄生式,parasitic。
思路与寄生构造函数和工厂模式相似,建立一个仅用于封装继承过程的函数,在函数内部以某种方式来加强对象。
可是也会由于作不到函数复用而下降效率。
适用于主要考虑对象而不是自定义类型和构造函数的状况:
function createAnother(original) { // 经过调用函数建立一个新对象,不必定使用object()函数 var clone = object(original); // 以某种方式加强这个对象 clone.sayHi = function() { alert("hi"); }; // 返回这个对象 return clone; } var person = { name: "Nicholas", friends: ["Shelby", "Court", "Van"]; }; var anotherPerson = createAnother(person); anotherPerson.sayHi(); // "hi"
对于为何要寄生组合式继承,看了这篇文章还有知乎上的一些回答,主要的优点是组合继承两次调用了构造函数,而寄生只使用了一次。
刚开始不理解的是,为何在建立超类型原型副本时对超类型原型的实例化就不算调用构造函数呢?
后来仔细想了一下,的确能够不算调用了构造函数——
object()函数内的临时类型F的构造函数为空(function F() {}
),所以能够忽略不计。
如下是代码:
function object(o) { // 主要区别就是这里,构造函数的不一样 function F() {} F.prototype = o; return new F(); } function inheritPrototype(subType, superType) { var prototype = object(superType.prototype); // 拷贝原型 prototype.constructor = subType; // 弥补因重写prototype而失去的默认的constructor属性 subType.prototype = prototype; // 替换子类型原型 } function SuperType(name) { this.name = name; this.colors = ["red", "green", "blue"]; } SuperType.prototype.sayName = function() { alert(this.name); }; function SubType(name, age) { // 继承属性 SuperType.call(this, name); this.age = age; } // 寄生组合式继承 inheritPrototype(SubType, SuperType); SubType.prototype.sayAge = function(){ alert(this.age); };
没有名字的函数,也成为拉姆达(lamda)函数。
像
var functionName = function(arg0, arg1, arg2) { // 函数体 }
这样的函数表达式至关于建立了一个匿名函数,而后将这个匿名函数赋给一个变量。
将函数做为参数传入另外一个函数,或者从一个函数中返回另外一个函数时,一般都是用匿名函数。
(虽然不知道为何这本书要在这里再讲一遍这个,也许可能意思是callee指向的其实是匿名函数,无论怎么样复习一下callee吧)
前边在讲到函数内部对象arguments的属性callee(指向拥有这个arguments的函数)时有提到过递归阶乘函数这个例子:
function factorial(num) { if (num <= 1) { return 1; } else { return num * arguments.callee(num-1); // 建议 // return num * factorial(num-1); // 不建议 } } var anotherFactorial = factorial; factorial = null; anotherFactorial(4); // 使用callee这里结果为24, 函数内使用factorial这里会出错
有些人会分不清闭包和匿名函数。
闭包指的是有权访问另外一个函数做用域的函数。
建立闭包的常见方式是在一个函数内部建立另外一个函数。
首先先回顾一下做用域链(scope chain)。
当一个函数第一次被调用时,会建立一个执行环境(execute context)及相应的做用域链,并将做用域链赋值给一个特殊的内部属性[[Scope]]。
而后,使用this、arguments和其余命名参数的值来初始化函数的活动对象(activation object)。
这个活动对象处于做用域链的顶端,外部函数的活动对象处于第二位,外部函数的外部函数的活动对象处于第三位,... ... 直到全局执行环境的变量对象处于做用域链终点。
通常来讲,当函数执行完毕后,局部活动对象就会被销毁,内存中仅保存全局做用域(全局执行环境的变量对象)。
可是,闭包的状况又有所不一样。
在另外一个函数内部定义的函数会将外部函数的活动对象添加到它的做用域链中,当外部函数执行完毕后,若是内部的这个函数还未执行,即其做用域链还在引用外部函数的活动对象时,这个活动对象就不会被销毁。
知道内部的这个函数执行完毕,外部函数的活动对象才会随之一块儿销毁。
因为闭包会携带包含它的函数的做用域,所以回比其它函数占用更多内存,所以建议只有在必要时再考虑使用闭包。
做用域链的这种配置机制有一个反作用:闭包只能取得包含函数的任何变量的最后一个值。
function createFunctions() { var result = new Array(); for (var i = 0; i < 10; i++) { result[i] = function() { return i; }; } return result; } var funcs = createFunctions(); // 每一个函数都输出10 for (var i = 0; i < funcs.length; i++) { document.write(funcs[i]() + "<br />"); }
由于每一个函数的做用域链都保存着createFunctions()的活动对象,所以它们引用的都是同一个变量i,
当createFunctions()函数返回后,变量i的值为10,
因此每一个函数内部的i都是10。
能够经过建立另外一个匿名函数强制让闭包行为符合预期:
for (var i = 0; i < 10; i++) { result[i] = (function(num) { return function(){ return num; }; })(i); }
在这里,定义了一个当即执行的匿名函数,并将它的结果赋给数组。
在当即执行时,传入了变量i,又由于函数参数是按值传递的,所以就会将i的当前值赋给num。
而这个函数内部,又建立并返回了一个访问num的闭包。
这样,result数组中每一个函数都有一个本身的num变量的副本,就能够返回不一样的值了。
在闭包中使用this对象也可能致使一些问题。
this对象是在运行时基于函数的运行环境绑定的:
匿名函数的执行环境具备全局性,若是经过call()或者apply()改变环境执行环境,this会指向其余环境,但一般this指向window。
arguments也有一样的问题,
所以若是想访问做用域中的this和arguments对象,必须将对它们的引用保存到另外一个闭包可以访问的变量中,而后就可让闭包访问该对象了,以this为例:
var name = "The Window"; var object = { name: "My Object", getNameFunc1: function() { return function() { return this.name; } }, getNameFunc2: function() { var that = this; return function() { return that.name; } } }; object.getNameFunc1(); // "The Window" object.getNameFunc2(); // "My Object"
因为IE对JScript对象和COM(组件对象模型)对象使用不一样的垃圾收集例程,所以闭包在IE中可能会致使问题。
若是闭包的做用域链中保存着一个HTML元素,那么就意味着该元素没法被销毁:
function assignHansdler() { var element = document.getElementById("someElement"); element.onclick = function() { alert(element.id); }; }
以上代码建立了一个做为element元素事件处理程序的闭包,而这个闭包又建立了一个循环引用。
因为匿名函数保存了一个对assignHandler()的活动对象的引用,所以就会致使没法减小element的引用数。
只要匿名函数存在,element的引用数至少也是1,所以它占用的内存永远都不会被回收。
能够用以下方式解决:
function assignHansdler() { var element = document.getElementById("someElement"); var id = element.id; element.onclick = function() { alert(id); }; element = null; }
这样就消除了循环引用。
须要注意的是,即便闭包不直接引用element,包含函数的活动对象中也仍然会保存一个引用。
所以 ,有必要把element设为null。
JavaScript在遇到屡次声明一个变量的状况时,会自动忽略后边的声明,可是会执行后边声明中的初始化。
JavaScript没有块级做用域的概念,
所以块语句中定义的变量,其实是在包含函数中而不是语句中建立的。
能够用匿名函数来模仿块级做用域(私有做用域)来避免这个问题:
(function() { // 块级做用域 })();
须要注意的是,JavaScript将function当作一个函数声明的开始,而函数声明后边是不能跟括号的。
所以上边代码中函数外面包括的括号不能省略。这样能够把函数声明转换成函数表达式。
不管在什么地方,只要临时须要一些变量,就可使用私有做用域。
在匿名函数中的任何变量,都会在执行结束时销毁。
咱们应该经过创造私有做用域来尽可能少地向全局做用域添加变量和函数,以避免致使命名冲突。
除了前边提到的稳妥构造函数模式,还能够:
在构造函数中定义特权方法:
function MyObject() { // 函数的私有变量 var privateVariable = 10; // 函数的私有函数 function privateFunction() { return false; } // 特权方法 this.publicMethod = function() { privateVariable++; return privateFunction(); }; }
在建立MyObject实例后,除了publicMethod没有任何方法能够直接访问privateVariable和privateFunction()。
或者利用私有和特权成员,隐藏那些不该该被直接修改的数据:
function Person(name) { this.getName = function() { return name; }; this.setName = function(value) { name = value; } } var person = new Person("Nicholas"); person.getName(); // "Nicholas" person.setName("Greg"); person.getName(); // "Greg"
私有变量name在每个实例的做用域中都不相同,由于每次调用构造函数都会从新建立这两个方法。
可是这样使用构造函数会有构造函数模式的缺陷:没法方法复用。每次建立实例都会建立一样一组方法,用静态私有变量来实现特权方法就能够解决这个问题。
(function() { var name = ""; // 没有使用var声明,所以为全局变量 Person = function(value) { name = value; } Person.prototype.getName = function() { return name; } Person.prototype.setName = function(value) { name = value; } })(); var person1 = new Person("Nicholas"); person1.getName(); // "NIcholas" person1.setName("Greg"); person1.getName(); // "Greg" var person2 = new Person("MIchael"); person1.getName(); // "MIchael" person2.getName(); // "MIchael"
在这种模式下,name就变成了静态的、由全部实例共享的属性。
所以每次改变name改变的是全部实例的name。
这样创造静态私有变量会由于使用原型而增进代码复用,但每一个实例都没有本身的私有变量。
所以使用哪一个方法还要视具体状况而定。
多查找做用域链的一个层次会必定程度上影响查找速度,这正是闭包和私有变量的一个明显的不足之处。
对于私有变量,我认为可使用二者组合的模式,不知道对不对,这里贴出想法,欢迎指正(zmj原创,转载需注明出处):
function Person(name) { this.getName = function() { return name; }; this.setName = function(value) { name = value; } } (function() { var teacher = "Nicholas"; // 初始化 Person.prototype.getTeacher = function() { return teacher; } Person.prototype.setTeacher = function(value) { teacher = value; } })();
这样,就既有实例本身的私有变量,也有静态私有变量了。
模块模式(module pattern)是为单例(singleton)建立私有变量和私有方法。
所谓单例就是只有一个实例的对象,通常以对象字面量的方式来建立:
var singleton = { name: value, method: function() { // 这里是方法的代码 } };
模块模式经过为单例添加私有变量和特权方法来使其加强:
var singleton = function() { // 私有变量和私有函数 var privateVariable = 10; function privateFunction() { return false; } // 特权/公有方法和属性 return { publicProperty: true, publicMethod: function() { privateVariable++; return privateFunction(); } }; }();
这种模式在须要对单例进行某些初始化,同时又须要维护其私有变量时是十分有用的:
function BaseComponent() {} function OtherComponent() {} var application = function() { // 私有变量和函数 var components = new Array(); // 初始化 components.push(new BaseComponent()); // 公共 return { getComponentCount: function() { return components.length; }, registerComponent: function(component) { if (typeof component == "object") { components.push(component); } } }; }(); application.registerComponent(new OtherComponent()); application.getComponentCount(); // 2
在Web应用程序中,常用一个单例来管理应用程序级的信息。
以这种模式建立的单例都是Object的实例。
若是单例必须是某种类型的实例,还必须添加某些属性和/或方法加以加强,可使用加强的模块模式:
function BaseComponent() {} var application = function() { // 私有变量和函数 var components = new Array(); // 初始化 components.push(new BaseComponent()); // 创造application的一个局部副本 var app = new BaseComponent(); // 公共接口 app.getComponentCount: function() { return components.length; }; app.registerComponent: function(component) { if (typeof component == "object") { components.push(component); } }; // 返回这个副本 return app; }();