1. 程式人生 > >JavaScript面向物件精要(二)

JavaScript面向物件精要(二)

四、建構函式和原型物件

1. 建構函式

建構函式就是用new建立物件時呼叫的函式。使用建構函式的好處在於所有用同一個建構函式建立的物件都具有同樣的屬性和方法。

function Person(){}
var p1 = new Person();
console.log(p1.constructor === Person); // true
console.log(p1 instanceof Person);      // true

可以使用建構函式來檢查一個物件的型別,但還是建議使用instanceof來檢查物件型別。因為建構函式屬性可以被覆蓋,並不一定完全準確。
示例:建構函式返回物件

function Foo(){
    this.name = "foo";
    return {name: "hhh"};
}
var f = new Foo();
f.name;         // hhh
f.constructor;  // Object

示例:建構函式返回原始型別

function Too(){
    this.name = "too";
    return "hhh";
}
var t = new Too();
t.name;         // too
t.constructor;  // Too

建構函式中顯示呼叫return:

  • 如果返回的值是一個物件,它會替代新建立的物件例項返回;
  • 如果返回的值是一個原始型別,它會被忽略,新建立的物件例項會被返回。

2. 原型物件

3. 改變原型物件

[[Prototype]]屬性只是包含一個指向原型物件的指標,並不是一個副本;任何對原型物件的改變都立刻反映到所有引用它的物件例項上。
示例:擴充套件原型物件

function Person(){}
var p = new Person();
p.sayHi();  //  p.sayHi is not a function(…)
Person.prototype.sayHi = function(){
    console.log("hi");
};
p.sayHi();  // "hi"

注意:在一個物件上使用Object.seal()Object.freeze()時,完全是在操作物件的自有屬性,可以通過在原型上新增屬性來擴充套件這些物件例項。
示例:凍結物件

function Person(name){}
var p = new Person();
Object.freeze(p);
p.name = "ligang";
Person.prototype.sayHi = function(){
    console.log("hi");
};
console.log(p.name);    // undefined
p.sayHi();  // "hi"

五、繼承

JavaScript內建的繼承方法被稱為原型物件鏈,又稱為原型物件繼承。當一個物件的[[Prototype]]設定為另一個物件時,就在這兩個物件之間建立了一條原型物件鏈。

1. Object.prototype

所有物件都繼承自Object.prototype

方法 說明
hasOwnProperty() 檢查是否存在一個給定名字的自有屬性
propertyIsEnumerable() 檢查一個自有屬性是否可列舉
isPrototypeOf() 檢查一個物件是否是另一個物件的原型物件
valueOf() 返回一個物件的值表達
toString() 返回一個物件的字串表達

上述5種方法經由繼承出現在所有物件中。
(1)valueOf()
valueOf()預設返回物件例項本身,可以定義自己的valueOf()方法,定義的時候沒有改變操作符的行為,僅僅定義了操作符預設行為所使用的值。
(2)toString()
一旦valueOf()返回的是一個引用而不是原始值的時候,就會回退呼叫toString()
示例:

var obj1 = {
    valueOf: function(){
        return "valueOf";
    },
    toString: function(){
        return "toString";
    }
}
var obj2 = {
    valueOf: function(){
        return {name: "哈哈"};
    },
    toString: function(){
        return "toString";
    }
}
obj1 + "";  // "valueOf"
obj2 + "";  // "toString"

2. 修改Object.prototype

Object.prototype新增方法,預設是可列舉的,意味著可以出現在for-in迴圈中。Douglas Crockford(JavaScript之父)推薦在for-in迴圈中始終使用hasOwnProperty()

var empty = {};
Object.prototype.myName = "LIGANG";
for(var prop in empty){
    console.log(prop);      // 會輸出:myName
}
for(var prop in empty){
    if(empty.hasOwnProperty(prop)){
        console.log(prop);  // 無任何內容輸出
    }
}

所以,在進行for-in操作時,最好的方式就是增加hasOwnProperty()判斷;與此時同,不要修改Object.prototype

3. 物件繼承

物件繼承是最簡單的繼承型別,需要做的就是指定哪個物件是新物件的[[Prototype]]
建立物件過程中,字面量形式會隱式指定Object.prototype為其[[Prototype]],也可以用Object.create()方式顯示指定。

var obj1 = {};
var obj2 = Object.create(Object.prototype, {});

Object.create()方法,第一個引數是需要設定為新物件的[[Prototype]]的物件,第二個引數是一個屬性描述物件,格式同Object.defineProperties()
示例:原型物件鏈(繼承)

var person = {
    name: "person",
    sayName: function(){
        console.log(this.name);
    }
};
var p1 = Object.create(person, {
    name: {
        cofigurable: true,
        enumerable: true,
        value: "ligang",
        writable: true
    }
});     

原型物件鏈
末端通常是一個Object.prototype[[prototype]]被置為null。
示例:空物件

var obj1 = {};
var obj2 = Object.create(null);
"toString" in obj1; // true
"toString" in obj2; // false

obj2沒有原型物件鏈的物件。完全是一個沒有任何預定義屬性的白板,使其成為一個完美的雜湊容器。

4. 建構函式繼承

建構函式的所有物件例項共享同一個原型物件,所以它們都繼承自該物件,但不能用原型物件繼承自有屬性。

function Person(name){
    this.name = name; // 私有屬性
}
Person.prototype.sayName = function(){  // 共用方法
    console.log(this.name);
};
var p1 = new Person("ligang");
var p2 = new Person("camile");

p1,p2都是建構函式Person的例項,其共享Person.prototype原型物件。但其自有屬性name不能通過原型物件繼承。
總結:自有屬性/方法通過建構函式定義,共有屬性/方法通過原型物件繼承!!!

六、物件模式

雖然JavaScript沒有一個正式的私有(區域性)屬性的概念(ES6中出現了let語法,可以定義區域性變數),但是可以建立僅在物件內可以訪問的資料或函式。使用模組模式可對外界隱藏資料;也可以使用立即呼叫函式表達(IIFE)定義僅可被新建立的物件訪問的本地變數和函式。

1. 私有成員和特權成員

var obj = (function(){
    // 私有變數
    var author = "ligang";
    return {
        // 公共的方法和屬性
        getAuthor: function(){
            return author;
        }
    };
}());
obj.author; // undefined
obj.getAuhtor(); // "ligang"

上述函式僅存在於被呼叫的瞬間,一旦執行後立即就被銷燬了。

// 上述示例的等價寫法
var obj = (function(){
    // 私有變數
    var author = "ligang";
    var getAuthor = function(){
        return author;
    };
    return {
        // 公共的方法和屬性
        getAuthor: getAuthor
    };
}());

2. 混入

混入將一個屬性從一個物件複製到另一個,從而使得接受者在不需要繼承提供者的情況下獲得其功能。和繼承不同,混入令你在建立物件後無法檢查屬性來源。若你想要獲得更強大的功能且需要知道該功能來自哪裡,繼承是首選!

function mixin(des, src){
    for(var prop in src){
        if(src.hasOwnProperty(prop)){
            des[prop] = src[prop];
        }
    }
    return des;
}
var a = {x:{y:1, z:3}};
mixin(a, {x:{y:2}, z:2}, function(a,b){
    try{
        return mixin(a,b, arguments.callee)
    }catch(ex){
        return b
    }
});

等價於 jQuery.extend(true, a, {x:{y:2}, z:2});

3. 作用域安全的建構函式

function Person(name){
    this.name = name;
}
var p1 = new Person("ligang");
var p2 = Person("camile");
console.log(p1 instanceof Person); // true
console.log(p2 instanceof Person); // false

作用域安全的建構函式是用不用new都可以被呼叫來生成新的物件例項的建構函式。其this在建構函式一開始執行就已經指向自定義型別的例項。當然,你可以根據new的使用與否決定建構函式的行為。

function Person(name){
    if(this instanceof Person){
        this.name = name;
    }else {
        return new Person(name);
    }

}
var p = Person("ligang");
console.log(p instanceof Person); // true

許多內建建構函式,例如Array、RegExp不需要new也可以工作,正是因為它們被設計之初採用了作用域安全的建構函式。