美文网首页JavaScriptWeb前端之路前端开发那些事
你根本不懂Javascript(3):类和模块

你根本不懂Javascript(3):类和模块

作者: szhielelp | 来源:发表于2017-03-21 22:43 被阅读75次

    本文最初发布于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;
    } ());
    

    相关文章

      网友评论

        本文标题:你根本不懂Javascript(3):类和模块

        本文链接:https://www.haomeiwen.com/subject/gxrnnttx.html