JavaScript之安全作用域的構造函數(高級函數)

函數是JavaScript最有趣的部分之一,它可以是最簡單的函數,也可以是最複雜的函數。一些額外的功能還可以通過閉包來實現。由於函數也是對象,所以通過函數指針來操作函數是最非常簡單的,以下是幾種函數使用的高級技巧。


安全的類型檢測

JavaScript內置的類型檢測機制並非完全可靠。如:intsanceof操作符在存在多個全局作用域的情況下,也是有問題的。
var arr = ["1", "2", "hello"];
console.log(arr instanceof Array); //true
以上代碼要想返回true,value必須是一個數組,並且與Array構造函數在同一個全局作用域下。如果value是在另一個frame中定義的,那麼以上代碼會返回false。

在檢測某個對象到底是原生對象還是開發人員自定義對象時也有問題。原因是因爲瀏覽器也可以支持JSON對象了,JSON庫定義了一個全局對象,於是開發人員很難確定JSON對象是不是原生對象。

解決上述問題的方法就是:我們知道任何值調用Object原生的toStiring()方法(即:Object.ptorotype.toString())都會返回一個[object xxx]的字符串,用以確定該對象屬於哪一種原生構造函數,返回原生構造函數名。變量對象可以使用Object的原生方法toString來檢測。用call()方法。
如:
      var arr = ["red", "hello", 1, 2, true]; 
      console.log(Object.prototype.toString.call(arr)); //[object Array]
用這個方法檢測arr,返回一個"[object Array]",說明該變量屬於原生數組對象。

由於原生構造函數名與全局作用域無關,因此可以使用toSting()方法保證返回一致的值。

利用這一點,可以創建以下函數用於檢測一個對象是否是原生數組對象
 //該函數用於檢測某變量是否是原生數組對象
 function isArray (value) {
     return (Object.prototype.toString.call(value));
 }
 
 var arr = ["red", "hello", 1, 2, true]; 
 console.log(isArray(arr)); //[object Array]

同理,也可以創建一個函數用於檢測某函數是原生對象還是自定義對象

//該函數用於檢測某變量是否是原生函數
function isFunction (value) {
    return (Object.prototype.toString.call(value));
}

var func = function () {alert("hi");};
console.log(isFunction(func)); //[object Function]

也可以創建一個函數用於檢測某正則表達式是否是原生正則表達式。
 //該函數用於檢測某正則表達式是否是原生正則表達式
 function isRegExp (value) {
     return (Object.prototype.toString.call(value));
 }
 
 var parent = /.a/gi;
 console.log(isRegExp(parent)); //[object RegExp]


以上技巧也應用於檢測JSON對象。Object的原生的toString()方法不能 檢測非原生構造函數的構造函數名,也就是說開發人員自定義的對象屬於非原生對象,該方法返回"[object Object]"。

如:
//該函數用於檢測某對象是否是原生對象
function isObject (value) {
    return (Object.prototype.toString.call(value));
}

var obj = {naem: "tom", age: 21}
console.log(isObject(obj)); //[object Object] 非原生對象

檢測JSON對象:
 //該函數用於檢測某對象是否是原生對象
 function isObject (value) {
     return (Object.prototype.toString.call(value));
 }
 
 var isJSON = window.JSON;
 var json = Object.prototype.toString.call(JSON);
 console.log(isObject(isJSON)); //[object JSON] 
 console.log(isObject(json)); //[object String]

在WEB開發中區分原生對象與非原生對象是非常重要的。


作用域安全的構造函數

我們再次回故前面所講的使用new關鍵字和構造函數名來創建一個新的實例對象的過程:

function Person (name, age, job) {
    this.name = name;
    this.age = age;
    this.job = job;
    
    this.sayName = function () {
        console.log(this.name);
    };
}

//使用new實例化一個對象
var person1 = new Person("Tom", 21, "WEB前端");

分離構造器:
使用new關鍵字創建對象要經歷四個步驟:

1、首先是創建一個對象:var person = {};

2、其次將構造函數的作用域賦值給新的對象(this對象就指向了這個新對象)那麼this也就指向了這個構造函數(這是某實例對象是否爲這個構造函數的實例的關鍵點)

3、執行構造函數中的代碼。Person中的代碼

4、返回這個新對象。

最後一步就說明了,我們只要返回這個新對象即可。實際上,關鍵點就是new操作符就是將原型鏈與實例的this對象聯繫起來。所以說如果想有原型鏈就必須用new操作符,否則this就變成了window對象了。



構造函數就是使用關鍵字new調用的函數。當使用new調用時,構造函數內部的this對象會指向新創建的實例對象。

下面就定義一個構造函數並實例化一個對象:
function Person (name, age, job) {
    this.name = name;
    this.age = age;
    this.job = job;
    
    this.sayName = function () {
        console.log(this.name);
    };
}

//使用new實例化一個對象
var person1 = new Person("Tom", 21, "WEB前端");

console.log(person1.age); //21person1.sayName(); //Tom


Person構造函數使用this對象給三個屬性賦值:name、age和job。當使用new創建新對象時,this對象就會指向這個新對象,同時給它分配這個三個屬性。


如果沒有使用new關鍵字來調用該構造函數,直接就使用Person構造函數來創建新對象,由於this對象是在運行時才與新對象綁定的,所以直接調用Person(),this對象將映射到window對象上,即直接指向window對象,導致將Person構造的屬性添加到window對象上。

如:
function Person (name, age, job) {
    this.name = name;
    this.age = age;
    this.job = job;
    
    this.sayName = function () {
        console.log(this.name);
    };
}

//使用new實例化一個對象
var person1 = new Person("Tom", 21, "WEB前端");
console.log(person1.age); //21
person1.sayName(); //Tom

//不使用new關鍵字,直接調用Person(),將屬性添加到了window對象上
var person2 = Person("Bob", 22, "Doctor");
console.log(window.name); //Bob
console.log(window.age); //22
window.sayName(); //Bob

原本是給person實例的三個屬性被添加到了window對象上,沒有使用new操作符,這裏的構造函數是作爲普通函數調用的,即"var person = Person()"就相當於“Person()”,this對象晚綁定,解析器將this對象解析爲window對象了。原來的window的name屬性量用於標識鏈接目標和frme的,這裏name屬性將覆蓋window對象原來的屬性,因爲所導致其它錯誤。

解決上述問題的方案就是:創建一個作用域安全的構造函數也就是能讓this對象指向正確的實例對象。首先判斷this對象是否爲正確類型的實例(this對象是否指向構造函數),即判斷this對象是不是Person類型的實例,如果不是,則創建新的實例並返回。提示:使用instanceof方法來判斷this對象是否屬於正確的類型實例(也就是構造函數的實例)。上述例子錯誤的原因就是將this對象解析爲了window對象,window對象不屬於Person構造函數的實例類型。所以在進行構造函數的一切操作前,先判斷this對象是否爲某構造函數的實例。

function Person (name, age, job) {
    //首先檢測this對象是否爲正確類型的實例,即是否爲Person類型的實例。
    if (this instanceof Person) { //現有的實例環境中,判斷this是否指向Person
        this.name = name;
        this.age = age;
        this.job = job;
    
        this.sayName = function () {
            console.log(this.name); //返回一個新的實例(那麼以上賦值操作就沒有執行)
        };
    } else {
        return new Person(name, age, job); //如果檢測出this不是正確類型的實例則創建實例並返回
    }
}

//首先使用new操作符創建新實例
var person1 = new Person("Tom", 21, "WEB前端");
console.log(person1.age); //21
person1.sayName(); //Tom

//接着不使用new,直接調用Person()作爲普通函數。
var person2 = Person("Bob", 22, "Doctor");
console.log(person2.name); //Bob
console.log(person2.age); //22
person2.sayName(); //Bob

通過判斷this對象是否是構造函數的實例,來確定由該構造函數創建的對象是否是該構造函數的實例。
這裏添加了一段if()語句用於判斷this對象是否爲Person構造函數的實例,它表示要麼使用new關鍵字創建實例,要麼在現有的Person實例環境中調用構造函數。如果this對象不是Person的實例,那麼就會再次使用new操作符調用Person構造函數返回結果。所以最後不管是否使用new關鍵字調用Person構造函數,都會返回一個Person的新實例。


構造函數竊取模式的繼承

構造函數竊取模式的繼承是JavaScript最常見的繼承方法之一,它的思想就是:在子類型構造函數中調用超類型構造函數。介它有一些缺陷。它依就不安全。
function Person (sides) {
    //首先檢測this對象是否爲Person的實例。
    if (this instanceof Person) { 判斷this是否指向Person
        this.sides = sides;

        this.getArea = function () {
            return 0;
        };
    } else {
        return new Person(sides); //如果檢測出this不是正確類型的實例則創建實例並返回
    }
}

function Rec (w, h) {
    Person.call(this, 2); //想借用構造函數模式來繼承Person的屬性和方法
    this.w = w;
    this.h = h;
    
    this.getArea = function () {
        return this.w * this.h;
    };
}

var rec = new Rec(10, 10);
console.log(rec.sides); //undefined

在以上代碼中,Person構造函數作用域是安全的,而Rec構造函數作用域不是安全的。在新創建一個Rec實例rec後,這個實例rec本該通過Person.call(this, 2)繼承Person的sides屬性的,但是,Person構造函數作用域是安全的,判斷出this對象不是Person的實例(此時的this是Rec實例中的this對象),因此構造函數會創建並返回一個新的Person實例,而不會把sides屬性賦值給this對象(也就是不會執行if(){}中的語句,而是執行else{}中的語句),所以Rec的this對象上沒有sides屬性。Rec的實例中也不會有sides屬性了。因此返回"undefined"。


構造函數竊取模式結合原型鏈實現繼承


通過借用構造函數模式與原型鏈模式的組合來實現繼承。
function Person (sides) {
    //首先檢測this對象是否爲正確類型的實例,即是否爲Person類型的實例。
    if (this instanceof Person) {
        this.sides = sides;

        this.getArea = function () {
            return 0;
        };
    } else {
        return new Person(sides); //如果檢測出this不是Person的實例,則創建實例並返回
    }
}

function Rec (w, h) {
    Person.call(this, 2); //借用構造函數模式繼承
    this.w = w;
    this.h = h;
    
    this.getArea = function () {
        return this.w * this.h;
    };
}

Rec.prototype = new Person(); //原型鏈實現繼承:原型對象等於另一個對象的實例。

var rec = new Rec(10, 10);
console.log(rec.sides); //2  sides的值

通過原型鏈繼承,使Rec的實例也變成Person的實例,“Person.call(this. 2)“會按原意執行,最終爲Rec添加了sides屬性,這樣Rec的實例也就繼承了Person的屬性sides。


借用構造函數實現繼承這個問題思路量:要通過if判斷語句這道關卡(也就是說當this指向Person時才能通過)。把Rec的實例也變成Person的實例,那麼this對象就指向了Person構造函數。

還有要時刻記住:通過new操作創建實例對象的過程。


發佈了109 篇原創文章 · 獲贊 56 · 訪問量 13萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章