红宝书系列读书笔记(二)

面向对象的JavaScript

ECMAScript中有两种属性:数据属性访问器属性
数据属性:configurable、enumerable、writable、value
只能使用Object.defineProperty()方法才可以修改属性默认的特性。
该方法接收三个参数:属性所在的对象,属性的名字和一个描述符对象
可以多次调用Object.defineProperty()方法修改同一属性,但在把configurable特性设置为false后就会有限制了。

访问器属性:configurable、enumerable、get、set
变量名以下划线开头(_year),用于表示只能通过对象方法访问的属性。
Object.defineProperties()可以一次定义多个属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
var book={};
Object.defineProperties(book, {
    _year: {
        value: 2004
    },
    edition: {
        value: 1
    },
    year: {
        get: function(){
            return this._year;
        },
        set: function(newValue){
            if(newValue > 2004){
                this._year = newValue;
                this.edition += newValue-2004;
            }
        }
    }
});

Object.getOwnPropertyDescriptor()方法可以取得给定属性的描述符,
接收两个参数:属性所在对象和要读取其描述符的属性名称

1
var descriptor=Object.getOwnPropertyDescriptor(book,"_year");

创建对象

工厂模式:抽象了创建具体对象的过程。(没有解决对象识别问题)

1
2
3
4
5
6
7
8
9
10
11
function createPerson(name,age,job){
    var o = new Object();
    o.name=name;
    o.age=age;
    o.job=job;
    o.sayName=function(){
        alert(this.name);
    }
    return o;
}
var person=createPerson("Peter",22,"Student");

构造函数模式:创建自定义的构造函数,从而定义自定义对象类型的属性和方法
function Person(name,age,job){
this.name=name;
this.age=age;
this.job=job;
this.sayName=function(){
alert(this.name);
}
}
var person=new Person(“Peter”,22,”Student”);

使用new操作符调用构造函数会经历4个步骤:
(1)创建一个新对象,分配堆内存;
(2)将构造函数的作用域赋给新对象(因此this就指向了这个新对象);
(3)执行构造函数中的代码(为这个新对象添加属性);
(4)返回新对象。

构造函数的缺点:每个方法都要在每个实例上重新创建一遍。
解决方法:将构造函数中的方法转移到构造函数外部,成为全局的函数方法。

原型模式:每个函数都有prototype属性,这个属性是一个指针,指向一个对象,
而这个对象的用途是包含可以由特定类型的所有实例共享的属性和方法。
使用原型对象的好处是可以让所有对象实例共享它所包含的的属性和方法。

1
2
3
4
5
6
7
8
9
function Person(){}
Person.prototype.name = "Peter";
Person.prototype.age = 22;
Person.prototype.job = "Student";
Person.prototype.sayName = function(){
    alert(this.name);
}
var person =new Person();
//Person.prototype.constructor指向Person.

ECMAScript 5 中增加一个新方法:Object.getPrototypeOf(),返回对象的原型。
不能通过对象实例重写原型中的值。
当为对象实例添加属性时,这个属性会屏蔽原型对象中保存的同名属性

使用hasOwnPrototype()方法可以检测一个属性时存在于实例中,还是存在于原型中。
这个方法只在给定属性存在于对象实例中时,才会返回true.

in操作符:会在通过对象能够访问给定属性时返回true,无论该属性存在于实例中还是原型中。(常用在for-in结构中)

1
2
3
4
//判断属性是在对象实例中,还是在原型中
function hasPrototypeProperty(object,name){
    return !object.hasOwnProperty(name) && (name in object);
}

要取得对象上所有可枚举的实例属性,可以使用ECMAScript 5中的Oject.keys()方法,这个方法接收一个对象作为参数,返回一个包含所有可枚举属性的字符串数组。
若想得到所有实例属性,无论它是否可枚举,都可以使用Object.getOwnPropertyNames()。
实例与原型之间是松散连接关系。
实例中的指针仅指向原型,而不指向构造函数。

  • 不推荐直接修改原生对象的原型,如果因某个实现中缺少某个方法,就在原生对象的原型中添加这个方法,那么当在另一个支持该方法的实现中运行代码时,就可能导致命名冲突。而且,这样做也可能会意外地重写原生方法。

原型对象的问题:因为所有实例都共享原型中的所有属性,所有无法做到实例的属性私有化。

组合使用构造函数模式和原型模式(使用最广泛的)
创建自定义类型的最常见方式,就是组合使用构造函数模式与原型模式。
构造函数模式用于定义实例属性,而原型模式用于定于方法和共享的属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
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);
 }
}

动态原型模式
把所有信息都封装在构造函数中,而通过在构造函数中初始化原型(仅在必要的情况下),又保持了同时使用构造函数和原型的优点。换句话说,可以通过检查某个应该存在的方法是否有效,来决定是否需要初始化原型。

1
2
3
4
5
6
7
8
9
10
11
function Person(name,age,job){
 this.name=name;
 this.age=age;
 this.job=job;
//检测是否存在sayName()方法
 if(typeof this.sayName != "function"){
    Person.prototype.sayName = function(){
        alert(this.name);
    }
 }
}

使用动态原型模式时,不能使用对象字面量重写原型。

寄生构造函数模式
创建一个函数,该函数的作用仅仅是封装创建对象的代码,然后再返回新创建的对象。

1
2
3
4
5
6
7
8
9
10
11
function Person(name,age,job){
    var o = new Object();
    o.name=name;
    o.age=age;
    o.job=job;
    o.sayName=function(){
        alert(this.name);
    };
    return o;
}
var person = new Person("Peter",22,"Student");

这个模式可以在特殊的情况下用来为对象创建构造函数。
假设我们想创建一个具有额外方法的特殊数组,由于
不能直接修改Array构造函数,可以使用这个模式:

1
2
3
4
5
6
7
8
9
10
function SpecialArray(){
    var values = new Array();
    values.push.apply(values,arguments);
    values.toPipedString = function(){
        return this.join("|");
    }
    return values;
}
var colors = new SpecialArray("red","green","blue");
alert(colors.toPipedString());

该模式下,返回的对象与构造函数或者与构造函数的原型属性之间没有关系,也就是说 ,构造函数返回的对象与在构造函数外部创建的对象没什么不同。

稳妥构造函数模式
稳妥对象,指的是没有公共属性,而且其方法也不引用this的对象。
稳妥构造函数与寄生构造函数类似,但有两点不同:
一是新创建的对象的实例方法不引用this;二是不适用new操作符调用构造函数。

1
2
3
4
5
6
7
8
9
10
function Person(name,age,job){
    var o = new Object();
    o.name = name;
    o.age = age;
    o.job = job;
    o.sayName=function(){
        alert(name);
    };
    return o;
}

继承

1.使用原型链实现继承
基本思想:利用原型让一个引用类型继承另一个引用类型的属性和方法(即让一个引用类型的原型指向了另一个引用类型)。
构造函数、原型和实例的关系:每个构造函数都有一个原型对象,原型对象都包含一个指向构造函数的指针,而实例都包含一个指向原型对象的内部指针。

实现原型链的基本模式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function SuperType(){
    this.property = true;
}
SuperType.prototype.getSuperValue = function(){
    return this.property;
};
function SubType(){
    this.subproperty = false;
}
//继承了SuperType
SubType.prototype = new SuperType();
SubType.prototype.getSubValue = function(){
    return this.subproperty;
};
var instance = new SubType();
alert(instance.getSuperValue());    //true

实现的本质:重写原型对象,代之以一个新类型的实例。
所有函数的默认原型都是Object的实例,因此默认原型都会包含一个内部指针,指向Object.prototype。这也是所有自定义类型都会继承
toString(),valueOf()等默认方法的根本原因。

确定原型和实例的关系:
instanceof和isPrototypeOf();

通过原型链实现继承时,不能使用对象字面量创建原型方法。因为这样做会重写原型链。

使用对象字面量创建的实例是object的实例。
使用原型链实现继承的问题:
(1)、所有原型链上的原型都共享属性;
(2)、在创建子类型的实例时,不能向超类型的构造函数中传递参数。

2.借用构造函数
基本思想:在子类型构造函数的内部调用超类型构造函数。
主要是使用apply(),call()。

1
2
3
4
5
6
7
8
function SuperType(){
    this.colors=["red","blue","green"];
}
function SubType(){
    //继承了SuperType
    SuperType.call(this);
}

借用构造函数有个优势,即可以在子类型构造函数中向超类型构造函数传递参数。

3.组合继承(也叫伪经典继承)最常用的继承模式
指的是将原型链和借用构造函数的技术组合到一块,从而发挥二者之长的一种继承模式。
基本思路:使用原型链实现对原型属性和方法的继承,而通过借用构造函数来实现对实例属性的继承。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
function SuperType(name){
    this.name = name;
    this.colors = ["red","blue","green"];
}
SuperType.prototype.sayName = function(){
    alert(this.name);
}
function SubType(name,age){
    //继承属性
    SuperType.call(this,name);
    this.age = age;
}
//继承方法
SubType.prototype = new SuperType();
SubType.prototype.constructor = SubType;
SubType.prototype.sayAge = function(){
    alert(this.age);
};
var instance = new SubType("Peter",22);
instance.colors.push("black");
alert(instance.colors);    //    "red","blue","green","black"
instance.sayName();    // Peter
instance.sayAge();    // 22

4.原型式继承
借助原型可以基于已有的对象创建对象,同时还不必因此创建自定义类型。

1
2
3
4
5
function object(o){
    function F(){}
    F.prototype = o;
    return new F();
}

在object()函数内部,先创建了一个临时性的构造函数,然后将传入的对象作为这个构造函数的原型,
最后返回这个临时类型的一个实例。从本质上讲,object()对传入其中的对象执行了一次浅复制。
就是将一个对象传入object()函数,然后根据具体需求对得到的对象加以修改。

ECMAScript 5定义了Object.create()来规范原型式继承。
这个方法接收两个参数:用作新对象原型的对象和(可选的)一个为新对象定义额外属性的对象。
在传入一个参数的情况下,Object.create()和上面的object()方法的行为相同。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var Person = {
    name : "Peter";
    friends : ["Shelby","Court","Van"];
};
var person1 = Object.create(Person);
person1.name = "Greg";
person1.friends.push("Rob");
var person2 = Object.create(Person);
person2.name = "Linda";
person2.friends.push("Barbie");
alert(Person.friends);    //"Shelby","Court","Van","Rob","Barbie"

5.寄生式继承
(类似于工厂模式)创建一个仅用于封装继承过程的函数,该函数在内部以某种方式来增强对象,最后在想真地是它做了所有工作一样返回对象。

1
2
3
4
5
6
7
function createAnother(original){
    var clone = object(original);    //这里的object()函数就是前面提到的原型式继承的方法    
    clone.sayHi=function(){           //以某种方式来增强这个对象
        alert("hi");
    }
    return clone;                             //返回这个对象
}

寄生式继承和构造函数模式类似,都会由于不能做到函数复用而降低效率。

6.寄生组合式继承(最理想的继承范式)
通过借用构造函数来继承属性,通过原型链的混成形式来继承方法。减少了一次调用SuperType构造函数。
基本思路:不必为了指定子类型的原型而调用超类型的构造函数,我们所需要的无非就是超类型原型的一个副本而已。
本质上,就是使用寄生式继承来继承超类型的原型,然后再将结果指定给子类型的原型。
基本模式如下:

1
2
3
4
5
function inheritPrototype(subType,superType){
    var prototype = Object.create(superType.prototype);        //创建对象
    prototype.constructor = subType;                    //增强对象
    subType.prototype = prototype;                      //指定对象
}

在函数内部,先是创建超类型原型的一个副本,然后为创建的副本添加constructor属性,
从而弥补因重写原型而失去的默认的constructor属性,
最后,将新创建的对象(即副本)赋值给子类型的原型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function SuperType(name){
    this.name = name;
    this.colors = ["red","blue","green"];
}
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);
};

函数表达式

定义函数的两种方式:一种是函数声明,另一种是函数表达式;
函数声明:function foo(){}
函数表达式:var foo = function(){}
(函数表达式就是把函数当作值来使用)

递归函数:
可以使用arguments.callee属性来实现:

1
2
3
4
5
6
7
function factorial(num) {
    if(num <= 1){
        return 1;
    }else{
        return num * arguments.callee(num-1);    //arguments.callee指向的是factorial()
    }
}

但在严格模式下,使用arguments.callee会导致错误,所以更好的递归实现是使用命名函数表达式:

1
2
3
4
5
6
7
var factorial  = (function f(num) {
    if(num <= 1) {
        return 1;
    }else {
        return num * f(num - 1);    
    }
});

闭包

闭包:是指有权访问另一个函数作用域中的变量的函数。
创建闭包的常见方式:在一个函数内部创建另一个函数
作用域链本质上是一个指向变量对象的指针列表,它只引用但不实际包含变量对象。
一般的,当函数执行完毕后,局部活动对象就会被销毁,内存中仅保存全局作用域,
但闭包的情况有所不同。

在一个函数内部定义的函数(内部函数)会将包含函数(外部函数)的活动对象添加到它的作用域链中。
当外部函数执行完毕后,其活动对象不会被销毁,因为内部匿名函数的作用域链仍然在引用这个活动对象。

由于闭包会携带包含它的函数的作用域,因此会比其他函数占用更多的内存。过度使用闭包可能会导致内存占用过多。
由于作用域链的配置机制,所以闭包只能取得包含函数中任何变量的最后一个值。
修改前:

1
2
3
4
5
6
7
8
9
10
11
function createFun() {
    var result = new Array();
    for(var i = 0; i < 10;i++) {
        result[i] = function() {
            return i;
        };
    }
    return result;
}
var arr=createFun();     //返回函数数组
console.log(arr[0]());   //执行保存在函数数组中的第一个函数,返回10

可以通过创建另一个匿名函数强制让闭包的行为符合预期:
修改后:

1
2
3
4
5
6
7
8
9
10
11
12
13
function createFun(){
    var result = new Array();
    for(var i = 0; i < 10;i++) {
        result[i] = function(num) {
            return function() {
                return num;
            };  
        }(i);
    }
    return result;
}
var arr=createFun();     //返回函数数组
console.log(arr[0]());   //执行保存在函数数组中的第一个函数,返回0

以上是《javascript高级程序设计 第3版》181页中的内容,大概是说修改前每个函数引用的是同一个变量i,所以返回的变量i是10;
而修改后使变量i传递到一个匿名函数里,然后通过创建闭包返回,所以函数数组中的每个函数都能返回各自的值。
若是想让result数组保存匿名函数返回的各自的值,那么代码如下:

1
2
3
4
5
6
7
8
9
10
function createFun(){
    var result=new Array();
    for(var i = 0; i<10;i++) {
        result[i]=(function() {
            return i;
        })();    //让匿名函数立即执行
    }
    return result;
}
console.log(createFun());    //返回[0,1,2,3,4,5,6,7,8,9]

关于this对象
匿名函数的执行环境具有全局性,因此其this对象通常指向window。

1
2
3
4
5
6
7
8
9
10
var name = "The Window";
var object = {
    name : "My Object",
    getNameFunc : function() {
        return function() {    //返回匿名函数
            return this.name;
        };
    }
};
alert(object.getNameFunc()());    //“The Window”

每个函数在被调用时都会自动取得两个特殊变量:this和arguments,
内部函数在搜索这两个变量时,只会搜索到其活动对象为止,因此无法直接访问外部函数中的这两个变量。可以通过将外部作用域中的this或arguments对象保存在一个闭包能够访问到的变量里:

1
2
3
4
5
6
7
8
9
10
11
var name="The Window";
var object = {
    name: "My Object",
    getNameFunc: function() {
        var that = this;
        return function() {    //返回匿名函数
            return that.name;
        };
    }
};
alert(object.getNameFunc()());    //"My Object"

意外改变this值的情况:

1
2
3
4
5
6
7
8
var name = "The Window";
var object = {
    name: "My Object",
    getName: function() {
        return this.name;
    }
}
(object.getName = object.getName)();

因为这个赋值表达式的值是函数本身,所以this的值不能得到维持,结果返回的是“The Window”.

内存泄漏
如果闭包的作用域链中保存着一个HTML元素,那么就意味着该元素无法被销毁:

1
2
3
4
5
6
function assignHandler(){
    var element = document.getElementById("someElement");
    element.onclick= function(){
        alert(element.id);
    };
}

以上代码创建了一个作为element元素事件处理程序的闭包,而这个闭包又创建了一个循环引用。
element的引用至少是1,所以占用的内存不会被回收。
修改:

1
2
3
4
5
6
7
8
function assignHandler(){
    var element = document.getElementById("someElement");    
    var id=element.id;
    element.onclick= function() {
        alert(id);
    };
    element = null;
}

模仿块级作用域
javascript没有块级作用域,javascript遇到多次声明的同一个变量,会对后续的声明视而不见。

1
2
3
4
5
6
7
8
9
function outputNumbers(count) {
    for(var i =0; i< count;i++) {
        alert(i);
    }
    var i;
    alert(i);    //输出10;
}
var count = 10;
outputNumbers(count);

使用匿名函数来模仿块级作用域,用作块级作用域(通常称为私有作用域)的匿名函数语法:

1
2
3
(function() {
    //块级作用域
})()

以上代码定义并立即调用了匿名函数,这样既可以执行其中的代码,又不会在内存中留下对该函数的引用,这是一个函数表达式。

1
2
3
4
5
6
7
8
9
function outputNumbers(count) {
    (function(){        //是闭包,实现了私有作用域
        for(var i =0; i< count;i++) {
            alert(i);
        }
    })();
  
    alert(i);    //导致一个错误!
}

私有变量
在任何函数中定义的变量,都可以认为是私有变量,因为不能在函数的外部访问这些变量。
私有变量包括函数的参数、局部变量和在函数内部定义的其他函数。
如果在函数内部创建一个闭包,那么闭包通过自己的作用域链也可以访问这些私有变量。
利用这一点,就可以创建用于访问私有变量的公有方法。

有权访问私有变量和私有函数的公有方法称为特权方法。
创建特权方法的两种方式:
一、在构造函数中定义特权方法

1
2
3
4
5
6
7
8
9
10
11
12
function Myobject() {
    var privateVariable = 10;
    
    function privateFunction() {
        return false;
    }
    //特权方法
    this.publicMethod = function() {
        privateVariable++;
        return privateFunction();
    }
}

静态私有变量
通过在私有作用域中定义私有变量或函数,也同样可以创建特权方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
(function(){
    var privateVariable = 10;
    function privateFunction() {
        return false;
    }
    //构造函数
    MyObject = function() {};
    
    //特权方法
    MyObject.prototype.publicMethod = function() {
        privateVariable++;
        return privateFunction();
    };
})();

函数声明只能创建局部函数;初始化未经声明的变量,总是会创建一个全局变量。
多查找作用域链中的一个层次,就会在一定程度上影响查找速度。

模块模式

前面的模式是用于为自定义类型创建私有变量和特权方法的,而道格拉斯提出的模块模式则是为了单例创建私有变量和特权方法。

所谓单例,指的就是只有一个实例的对象,按照惯例,Javascript是以对象字面量的方式创建单例对象:

1
2
3
4
5
6
var singleton = {
    name: value;
    method: function() {
        //code...
    }
}

模块模式通过为单例添加私有变量和特权方法能够使其得到加强:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var singleton = {
    var privateVariable = 10;
    function privateFunction() {
        return false;
    }
    //特权方法和属性
    return {
        publicProperty : true,
        publicMethod : function () {
            privateVariable++;
            return privateFunction();
        }
    }
}();

这个模块模式使用了一个返回对象的匿名函数。
由于对象是在匿名函数定义的,因此它的公有方法有权访问私有变量和函数。
从本质上讲,这个对象字面量定义的是单例的公共接口。
模块模式适用于在需要对单例进行某些初始化,同时又需要维护其私有变量时

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var application = function() {
    var components = new Array();
    //初始化
    components.push(new BaseComponent());
    //公共
    return {
        getComponentCount: function() {
            return components.length;
        },
        
        registerComponent: function () {
            if(typeof component == "object") {
                components.push(component);
            }
        }
    };     
}();

增强的模块模式
适合那些单例必须是某种类型的实例,同时还必须添加某些属性和(或)方法对其加以增强的情况。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var singleton = {
    var privateVariable = 10;
    function privateFunction() {
        return false;
    }
    //创建某种类型的对象
    var object = new CustonType();
    //添加特权/公有属性和方法
    object.publicProperty = true;
    object.publicMethod = function () {
            privateVariable++;
            return privateFunction();
    };
    //返回这个对象
    return object;
}();

坚持原创技术分享,您的支持将鼓励我继续创作!