你根本不懂Javascript(3):類和模組

weixin_33895657發表於2017-03-21

本文最初釋出於http://szhshp.org/tech/2017/02/18/JavaSprite.html
轉載請註明

類和模組

工廠函式

function range(from, to) {
    var r = inherit(range.methods);         //繼承對應的方法             
    r.from = from;
    r.to = to;

    return r;
};


range.methods = {

    includes: function (x) {
        return this.from <= x && x <= this.to;
    },

    foreach: function (f) {
        for (var x = Math.ceil(this.from); x <= this.to; x++) f(x);
    },
    toString: function () {
        return "(" + this.from + "..." + this.to + ")";
    }
}

// Here are example uses of a range object.
var r = range(1, 3);                        // Create a range object
r.includes(2);                              // => true: 2 在對應的範圍之內
r.foreach(console.log);                     // Prints 1 2 3
console.log(r);                             // Prints (1...3)
  • 這裡給range()函式定義了一個屬性range.method用於快捷地存放定義類的原型物件。
  • 類裡面的屬性fromto都是非共享屬性,是不可以繼承的

使用建構函式代替工廠函式

function Range(from, to) {      //注意這兒函式名首字母變為大寫了
    this.from = from;
    this.to = to;
}

Range.prototype = {         //通過prototype來給類定義方法
    includes: function (x) {
        return this.from <= x && x <= this.to;
    },

    foreach: function (f) {
        for (var x = Math.ceil(this.from); x <= this.to; x++) f(x);
    },
    toString: function () {
        return "(" + this.from + "..." + this.to + ")";
    } };

var r = new Range(1, 3);                        // 注意新建obj的時候需要用到關鍵字new
r.includes(2);                                  // => true: 2 is in the range
r.foreach(console.log);                         // Prints 1 2 3
console.log(r);                                 // Prints (1...3)
  • 普通函式方法一般都首字母小寫,但是構造方法需要首字母大寫
  • 其次呼叫的時候需要新增關鍵字new, 同時這種情況下就不需要呼叫inherit()方法了

Constructor屬性

var F = function() {};             // This is a function object.
var p = F.prototype;            // This is the prototype object associated with it.
var c = p.constructor;         // This is the function associated with the prototype.
c === F;                      // => true: F.prototype.constructor==F for any function
//對於任意函式,F.prototype.constructor == F

var o = new F();              // Create an object o of class F
o.constructor === F;          // => true: the constructor property specifies the class

類的擴充

簡單易懂,如果原型中不存在對應方法就初始化對應方法

//給ES3中的函式類新增bind方法
if (!Function.prototype.bind) {
    Function.prototype.bind = function (o /*, args */) {
        // Code for the bind method goes here... };
    }
}

簡單例子:

var n = 3;
n.times(function (n) {
    console.log(n + " hello");
});
//下面分別給Number,string/function等新增方法或屬性
Number.prototype.times = function (f, context) {
    var n = Number(this);
    for (var i = 0; i < n; i++) f.call(context, i);
};

String.prototype.trim = String.prototype.trim || function () {
    if (!this) return this;  // Don't alter the empty string
    return this.replace(/^\s+|\s+$/g, "");  // Regular expression magic
};

Function.prototype.getName = function () {
    return this.name || this.toString().match(/function\s*([^(]*)\(/)[1];
};

給原型新增的方法可以使得所有的物件都可以呼叫這個方法

類和型別

有三種方法用於檢測物件類:

instanceof/isprototypeof

缺點:

  • 無法通過物件獲得類名,只能檢測物件是否屬於特定類
  • 多視窗和多框架的Web應用中相容存在問題

Constructor

        function typeAndValue(x) {
            if (x == null) return "";
            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;
            }
        }

缺點:

  • 多視窗和多框架的Web應用中相容存在問題

注意case後面的表示式都是函式。如果使用typeof的話獲取到的結果會是字串,例如下文

function type(o) {
    var t, c, n;  // type, class, name
// Special case for the null value:
    if (o === null) return "null";
// Another special case: NaN is the only value not equal to itself:
    if (o !== o) return "nan";
// Use typeof for any value other than "object".
// This identifies any primitive value and also functions.
    if ((t = typeof o) !== "object") return t;
// Return the class of the object unless it is "Object".
// This will identify most native objects.
    if ((c = classof(o)) !== "Object") return c;
// Return the object's constructor name, if it has one
    if (o.constructor && typeof o.constructor === "function" && (n = o.constructor.getName())) return n;
// We can't determine a more specific type, so return "Object"
    return "Object";
}


// Return the class of an object.
function classof(o) {
    return Object.prototype.toString.call(o).slice(8, -1);
};
// Return the name of a function (may be "") or null for nonfunctions
Function.prototype.getName = function () {
    if ("name" in this) return this.name;
    return this.name = this.toString().match(/function\s*([^(]*)\(/)[1];
};

此外並非所有物件都有Constructor屬性,匿名函式就是個典型:

// This constructor has no name
var Complex = function (x, y) {
    this.r = x;
    this.i = y;
}
// This constructor does have a name
var Range = function Range(f, t) {
    this.from = f;
    this.to = t;
}

JS的物件導向技術

一個全面並且典型的純OOP例子:

function Set() {          // This is the constructor
    this.values = {};     // The properties of this object hold the set
    this.n = 0;           // How many values are in the set
    this.add.apply(this, arguments);  // All arguments are values to add
}

// Add each of the arguments to the set.
Set.prototype.add = function () {
    for (var i = 0; i < arguments.length; i++) {  // For each argument
        var val = arguments[i];                  // The value to add to the set
        var str = Set._v2s(val);                 // Transform it to a string
        if (!this.values.hasOwnProperty(str)) {  // If not already in the set
            this.values[str] = val;              // Map string to value
            this.n++;                            // Increase set size
        }
    }
    return this;                                 // Support chained method calls
};

// Remove each of the arguments from the set.
Set.prototype.remove = function () {
    for (var i = 0; i < arguments.length; i++) {  // For each argument
        var str = Set._v2s(arguments[i]);        // Map to a string
        if (this.values.hasOwnProperty(str)) {   // If it is in the set
            delete this.values[str];             // Delete it
            this.n--;                            // Decrease set size
        }
    }
    return this;                                 // For method chaining
};

// Return true if the set contains value; false otherwise.
Set.prototype.contains = function (value) {
    return this.values.hasOwnProperty(Set._v2s(value));
};

// Return the size of the set.
Set.prototype.size = function () {
    return this.n;
};

// Call function f on the specified context for each element of the set.
Set.prototype.foreach = function (f, context) {
    for (var s in this.values)                 // For each string in the set
        if (this.values.hasOwnProperty(s))    // Ignore inherited properties
            f.call(context, this.values[s]);  // Call f on the value
};

Set._v2s = function (val) {         //這是一個內部函式,當然例項物件無法呼叫這個方法
    switch (val) {
        case undefined:
            return 'u';          // Special primitive
        case null:
            return 'n';          // values get single-letter
        case true:
            return 't';          // codes.
        case false:
            return 'f';
        default:
            switch (typeof val) {
                case 'number':
                    return '#' + val;    // Numbers get # prefix.
                case 'string':
                    return '"' + val;    // Strings get " prefix.
                default:
                    return '@' + objectId(val); // Objs and funcs get @
            }
    }

    // For any object, return a string. This function will return a different
    // string for different objects, and will always return the same string
    // if called multiple times for the same object. To do this it creates a
    // property on o. In ES5 the property would be nonenumerable and read-only.
    function objectId(o) {
        var prop = "|**objectid**|";   // Private property name for storing ids
        if (!o.hasOwnProperty(prop))   // If the object has no id
            o[prop] = Set._v2s.next++; // Assign it the next available
        return o[prop];                // Return the id
    }
};
Set._v2s.next = 100;    // Start assigning object ids at this value.

另一種通過返回值設定類的方法

function Test() {
    var map = 1;
    function a(){
        map = 2;
    }
    function b(){
        console.log(map);
    }
    return{
        a:a,
        b:b
    }
}
var t = new Test()

對於後者:

  • 注意如果最後的return裡面包含了map那麼無論如何執行b()這個map的值都不會變, 因為返回的是一個Obj是額外空間
  • 當然這裡也可以不放返回值
  • 返回值的方法是為了閉合部分介面
  • 更大的區別是:很難重寫第二種模式裡面的方法

子類

原書中的子類內容比較累贅,可以歸納為以下幾步:

  1. 繼承prototype中定義的屬性和方法;
  2. 繼承建構函式中定義的屬性和方法;
  3. 修改子類的prototype物件的constructor指標
function Animal(name) {  
    this.name = name;  
}  
Animal.prototype.set = "female";
Animal.prototype.info = function () {
        console.log("animal");  
}

function People(name) {  
    this.name = name;  
}  
People.prototype = new Animal("animal");  // 繼承父類中定義的屬性和方法;
People.prototype.info = function() {  //重寫父類中定義的屬性和方法;
    console.log("peopel")  
};  

//Demo
var cat = new Animal('cat');

console.log(cat instanceof Animal);    //t
console.log(cat instanceof Object);    //t
console.log( typeof Animal.prototype);      //object  
console.log( typeof Animal.constructor);        //function  
console.log(Animal.prototype.constructor == Animal);    //true  


var mike = new People("mike");  
console.log(mike.sex);//female  
mike.info();//People  

console.log(mike instanceof People);    //t
console.log(mike instanceof Animal);    //t
console.log(mike instanceof Object);    //t
console.log( typeof Animal.prototype);      //object  
console.log( typeof Animal.constructor);        //function  
console.log(People.prototype.constructor == People);    //true  

類的封裝

簡單封裝方法:

  1. 使用var關鍵字設定私有屬性

  2. 阻止類的擴充套件:

    使用Object.seal()可以阻止給物件新增屬性並將已有的屬性設定為不可配置的,即不可刪除

    但是這種情況下依然可以修改屬性

    Object.seal(mike);
    mike.sex = 'male'; //仍然可以修改
    delete mike.sex; //Cannot delete property 'sex' 
    
  3. 阻止類的修改:

    Object.seal()類似不過Object.freeze方法將例項方法設定為不可寫的

    這種情況下修改對應方法將變得無效

    Object.seal(mike);
    mike.sex = 'male'; //不會報錯但是修改無效
    

模組化模式

首先我們來看看Module模式的基本特徵:

  1. 模組化,可重用
  2. 封裝了變數和function,和全域性的namaspace不接觸,鬆耦合
  3. 只暴露可用public的方法,其它私有方法全部隱藏

基本用法

var Calculator = function (eq) {
    //這裡可以宣告私有成員

    var eqCtl = document.getElementById(eq);

    return {
        // 暴露公開的成員
        add: function (x, y) {
            var val = x + y;
            eqCtl.innerHTML = val;
        }
    };
};


var calculator = new Calculator('eq');
calculator.add(2, 2);

匿名閉包

(function () {
    // ... 所有的變數和function都在這裡宣告,並且作用域也只能在這個匿名閉包裡
    // ...但是這裡的程式碼依然可以訪問外部全域性的物件
}());

注意,匿名函式後面的括號,這是JavaScript語言所要求的,因為如果你不宣告的話,JavaScript直譯器預設是宣告一個function函式,有括號,就是建立一個函式表示式,也就是自執行,用的時候不用和上面那樣在new了,當然你也可以這樣來宣告:

(function () {/* 內部程式碼 */})();

引用全域性變數

獲取全域性變數到匿名函式域

(function ($, YAHOO) {
    // 這兒$相當於全域性的jQuery
} (jQuery, YAHOO));//這兩個是全域性變數, 我們把它們放到這兒說明使用這兩個引數呼叫上面那個匿名函式

從匿名函式域設定全域性變數

var blogModule = (function () {
    var my = [1,2,3]

    return my;//其實只要把這些變數返回回去就行了, 之後blogModule就相當於my這個變數
} ());

當然return也可以返回一個object

var blogModule = (function () {
    var my = [1,2,3]

    return {
        my: my,
        you: null
    }
} ());

高階用法

對變數自身進行擴充套件

var blogModule = (function (my) { // 2. 這裡接收到了傳進來的blogModule並把blogModule命名為my
    var AddPhoto = function () {    // 3. 這裡給my新增了個函式, 因此blogModule也多了個函式
        console.log(123);
    };
    return {AddPhoto: AddPhoto};
} (blogModule)); //1. 這裡將blogModule傳了進去
blogModule.AddPhoto()// 4. 擴充套件完畢後就可以呼叫了

鬆耦合擴充套件

上面的擴充套件必須要先定義這個blogModule, 能否在未定義的時候初始化而在已定義的時候直接擴充套件來達到鬆耦合的目的呢:

var blogModule = (function (my) {

    // 新增一些功能   
    
    return my;
} (blogModule || {}));  

這樣可以英一順序載入module模式

緊耦合擴充套件

雖然鬆耦合擴充套件很牛叉了,但是可能也會存在一些限制,比如你沒辦法重寫你的一些屬性或者函式,也不能在初始化的時候就是用Module的屬性。緊耦合擴充套件限制了載入順序,但是提供了我們過載的機會,看如下例子:

var blogModule = (function (my) {
    var oldAddPhotoMethod = my.AddPhoto;

    my.AddPhoto = function () {
        // 過載方法,依然可通過oldAddPhotoMethod呼叫舊的方法
    };

    return my;
} (blogModule));

子模組

blogModule.CommentSubModule = (function () {
    var my = {};
    // ...

    return my;
} ());

相關文章