封装

上一篇文章间隔有出不多一个多月了,如今十分难题有了好心境,继续看书,写点感悟.
第三章报告的是js封装,像java,可以经由过程private关键字来声明一个要领使得只需该对象内部的代码才实行它,在js中没有如许的关键字,然则可以运用闭包来建立只允许从对象内部接见的要领和属性.比拟于private,闭包走了一条弯路却到达一样的效果.

接口在js封装中的作用

上一篇文章讲到接口情势是很多其他js设想情势的基本,它定义了两个对象间的关联,接口稳定那末关联的两边可以被替代,不肯定非得运用像第二章那样严厉的接口,而且应当防备公然定义于接口中的要领,不然其他对象可以会对那些并不属于接口的要领发生依靠,不安全,由于这些要领随时都可以转变或许被删除.一个抱负软件体系应当未一切类定义接口,这些类只向外界供应他们完成的接口中划定的要领,任何别的要领都留作自用.其一切属性都是私有的,外界只能经由过程接口中定义的存取要领举行操纵.

建立对象的基本情势

有3种,流派大开型只能供应公用成员;第二种,运用下划线来示意要领或许属性的私有性;第三种,经由过程闭包来建立真正私有的成员,这些成员只能经由过程特权要领接见.以Book 类为例 — 一个用来存储关于一本书的数据的类,并为其完成一个以 HTML 情势显现这些数据的要领.如今只需建立这个类, 下面是其他人在建立并运用实例:

//Book(isbn, title, author)
var jsDesignPatterns = new Book('978-7-115-19128-1', 'JavaScript 设想情势', 'Harmes, R.');
jsDesignPatterns.display();
//Outputs the data by creatingand populating an HTML element.

流派大开型对象

完成 Book 类最简朴的做法是按传统体式格局建立一个类,用一个函数来做其组织器.他的一切属性和要领都公然可接见,这些公用属性须要运用 this 关键字来建立:

var Book = function(isbn, title, author) {
    if (isbn == undefined) {
        throw new Error('Book constructor requires an isbn.');
        this.isbn = isbn;
        this.title = title || 'No title specified';
        this.author = author || 'No author specified';
    }
    Book.prototype.display = function () {
        ...
    };
}

在组织器中,isbn 必选, 由于 display 要领请求 book 对象都有一个准确的 isbn,不然就不能找到响应的图片,也不能天生一个用于购书的链接.title 和 author 参数都是可选的,要有默认值以防它们未被供应.
有个最大的题目,没有方法搜检 isbn 数据的完整性,毛病的 isbn 数据可以致使 display 要领失效.下面的版本强化了对 isbn 的搜检:

var Book = function(isbn, title, author) {
    if (!this.checkIsbn(isbn)) {
        throw new Error('Book: Invalid ISBN.');
    }
    this.isbn = isbn;
    this.title = title || 'No title specified';
    this.author = author || 'No author specified';
}
Book.prototype ={
    checkIsbn: function(isbn) {
        if(isbn == undefined || typeof isbn != 'string') {
            return false;
        }

        isbn = isbn.replace(/-/, '');// Remove dashes.
        if(isbn.length != 10 && isbn.length != 13) {
            return false;
        }

        var sum =0;
        if (isbn.length === 10) { // 10 digit ISBN.
            if (!isbn.match(/^\d{9}/)) { // Ensure characters 1 through 9 are digits.
                return false;
            }

            for (var i = 0; i < 9; i++) {
                sum += isbn.charAt(i) * (10 - i);
            }
            var checksum= sum % 11;
            if (checksum === 10){
                checksum = 'X';
            }
            if (isbn.charAt(9) != checksum) {
                return false;
            }
        }
        else { // 13 digit ISBN.
            if (!isbn.match(/^\d{12}/)) { // Ensure characters 1 through 12 are digits.
                return false;
            }

            for (var i = 0; i < 12; i++) {
                sum += isbn.charAt(i) * ((i % 2 === 0) ? 1 : 3);
            }
            var checksum = sum % 10;
            if (isbn.charAt(12) != checksum) {
                return false;
            }
        }

        return false; // All tests passed.
    },

    display: function () {
        ...
    }
};

checkIsbn 要领可以保证 ISBN 是一个具有准确位数和校验和的字符串.由于Book 类如今有两个要领,所以 Book.prototype 被设置为一个对象字面量,如许在定义多个要领的时刻就不必在每一个要领前面都加上Book.prototype.
如今保证了 display 要领可以一般事情,涌现了另一个题目,假定一本书可以会有多个版本,每一个版本都有自身的 ISBN,在实例化book对象今后直接修正 isbn 属性:

jsDesignPatterns.isbn = '978-0261103283';
jsDesignPatterns.display();

所以纵然在组织器中对数据完整性举行磨练,照样没法阻挠其他人给 isbn赋值,为了庇护内部数据,为每一个属性供应 accessor 取值器和 mutator 赋值器要领.经由过程运用赋值器,你可以在把一个新值真正赋给属性之前举行种种磨练.下面是加入了取值器合赋值器今后的新版 Book 对象:

var Publication = new Interface ('Publication', ['getIsbn', 'getTitle', 'setTitle', 'getAuthor', 'setAuthor', 'display']);

var Book = function (isbn, title, author) { // implements Publication
    this.setIsbn(isbn);
    this.setTitle(title);
    this.setAuthor(author);
}

Book.prototype = {
    checkIsbn: function (isbn) {
        ...
    },
    getIsbn: function () {
        return this.isbn;
    },
    setIsbn: function (isbn) {
        if (!this.checkIsbn(isbn)) {
            throw new Error('Book: Invalid ISBN.');
        }
        this.isbn = isbn;
    },
    getTitle: function () {
        return this.title;
    },
    setTitle: function (title) {
        this.title = title || 'No title specified';
    },
    getAuthor: function () {
        return this.author;
    },
    setAuthor: function (author) {
        this.author = author || 'No author specified';
    },

    diplay: function () {
        ...
    }
};

上述代码定义了一个接口,只须要运用这个接口中定义的要领与对象来打交道.另有一些对数占有庇护作用的取值器,赋值器要领,以及一些磨练要领.
然则依旧有缺点:供应了赋值器要领,但那些属性依然是公然的,可以被直接设置.而且没法庇护内部数据,取值器和赋值器要领也引入了较多代码(js 文件大小较为重要).

用定名范例辨别私用成员

依旧是上面谁人 snippet,只不过 setAttributes 的时刻一切要设置的属性和要领都加上了 下划线_ 前缀,示意它是私用属性和要领(js 中可以运用下划线和字母开首定名出有用变量).
下划线的这类用法表明一个属性或许要领仅供对象内部运用,直接接见它或许设置它可以会致使意想不到的效果,这有助于防备顺序员对它的无意运用,却不能防备故意运用运用.
这个范例只需在获得遵守时才有效果,并不是真正可以用来隐蔽对象内部数据的解决方案,重要适用于非敏感性的内部要领和属性.

作用域,嵌套函数和闭包

在议论这类真正的私用性要领和属性的完成手艺之前,我们先稳固一下相干基本.在 js 中,只需函数具有作用域,在一个函数内部声明的变量在函数外部没法接见,私用属性也是愿望没法在对象外部接见的变量,所以作用域相干性显著.定义在一个函数中的变量在该函数中的内嵌函数是可以接见的.

function foo() {
    var a = 10;
    function bar() {
        a *= 2;
    }

    bar();
    return a;
}

a定义在函数foo中,但函数 bar 可以接见它,由于 bar 也定义在 foo 中,bar 内部对 a 赋新值,当 bar 在 foo 中被挪用时它可以接见 a,这可以明白.假如 bar 在 foo 外部被挪用呢

function foo() {
    var a = 10;

    function bar () {
        a *= 2;
        return a;
    }

    return bar;
}
var baz = foo(); // baz is now a reference to functionbar.
baz(); // return 20;
baz(); // return 40;
baz(); // return 80;

var blat = foo(); // blat is another reference to bar.
blat(); // return 20, because a new copy of a is being used.

在上述代码中, 所返回的对 bar 函数的援用被赋给变量 baz.这个函数如今是在 foo 外部被挪用,但他依旧可以接见 a.这是由于 js 中的作用域是词法性的,函数是运行在定义他们的作用域中(本例中是 foo 内部的作用域),而不是运行在挪用他们的作用域中,只需 bar 被定义在 foo 中,他就可以接见在 foo 中定义的一切变量, 马上 foo 的实行已完毕.
在 foo 返回后,它的作用域被保留下来,然则只需他返回的谁人函数可以接见这个作用域.baz和 blat 各有这个作用域及 a 的一个副本,而且只需他们自身能对其举行修正.返回一个内嵌函数是建立闭包最经常使用的手腕.

用闭包完成私用成员

如今有了对闭包的明白今后再来议论一下我们方才要做的事: 须要建立一个只能在对象内部接见的变量.借助于闭包你可以建立只允许特定函数接见的变量,而且这些变量在这些函数的各次挪用之间依旧存在.为了建立私用属性,你须要在组织器函数的作用域中定义相干变量,这些变量可以被定义于该作用域中的一切函数接见,包括哪些特权要领:

var Book = function(newIsbn, newTitle, newAuthor){ // implements Punlication
    // Private attribute.
    var isbn, title, author;

    // Private method.
    function checkIsbn(isbn) {
        ...
    }

    // Privilleged methods.
    this.getIsbn = function (){
        return isbn;
    };
    this.setIsbn =function (newIsbn) {
        if (!checkIsbn(newIsbn) {
            throw new Error('Book: Invalid ISBN.');
        }
        isbn =newIsbn;
    };

    this.getTitle = function () {
        return title;
    };

    this.setTitle = function (newTitle) {
        title = newTitle || 'No title specified';
    };

    this.getAuthor = function () {
        return author;
    };

    this.setAuthor = function (newAuthor) {
        author = newAuthor || 'No author specified';
    };

    // Constructor code.
    this.setIsbn(newIsbn);
    this.setTitle(newTitle);
    this.setAuthor(newAuthor);
};

// Public, non-privileged methods.
Book.prototype = {
    display: function () {
        ...
    }
};

以上代码和之前的建立对象情势有什么差别呢,其他情况下我们在建立和援用对象的属性时总要运用 this 关键字.然则这个处所我们用 var 声明这些属性变量,它们只存在于Book 组织器中,checkIsbn 函数是私有要领.
须要接见这些变量和函数的要领只需声明在 Book 中,被称作特权要领(privileged method),他们是公有要领,之所以有 特权要领前面都用 this 关键词来协助声明, 是为了在对象外部可以被接见.这些要领定义于 Book 组织器的作用域中,所以它们可以接见到私有属性,援用这些属性时并没有运用 this关键词,由于他们没有公然.一切取值器和赋值器要领都被改成不加 this 的直接援用这些属性.
任何不须要直接接见私用属性的要领都可以像本来那样在 Book.prototype中声明.display 就是这类要领中的一个,他可以经由过程挪用 getIsbn或许 getTitle等等特权要领来间接接见任何私用属性.只需那些须要直接接见私用成员的要领才是特权要领.须要注重的是,特权要领太多会占用较多内存.每一个对象实例都包括了一切特权要领的新副本.
用闭包体式格局建立的对象可以具有真正的私有属性,其他顺序员不可以直接接见它们建立的Book 实例的任何内部数据.

然则,,,如许运用闭包照样有缺点的.之前的流派大开型对象建立情势中,一切要领都建立在原型对象中,因而不论天生若干对象实例,这些要领在内存中只存在一份.而在本节中每天生一个新的对象实例都将 copy 每一个私有要领和特权要领,,,如许就会带来更多内存斲丧,所以只合适用在真正私有成员的场所.这类对象建立情势也不利于派生子类,由于所派生出的子类不能接见超类的任何私有属性或许要领.
所以在 js 顶用闭包完成私有成员致使的派生题目被称作**”继续损坏封装(inheritance breaks encapsulation)”,假如你建立的类今后可以会须要派生出子类, 那末最好照样采纳前两种对象建立情势.

高等的对象建立情势

静态要领和属性

适才讲的作用域和闭包可用于建立静态成员(公有和私有),大多数要领和属性所关联的是类的实例,然则静态成员所关联的是类自身.也就是说,静态成员是在类的条理上操纵,不是实例条理上.每一个静态成员都只需一份,是经由过程类对象要领的.
下面是添加了静态属性和要领和 Book 类:

var Book = (function () {

    // Private static attributes.
    var numOfBooks = 0;

    // Private static method.
    function checkIsbn(isbn) {
        ...
    }

    // Return the constructor.
    return function(newIsbn, newTitle, newAuthor) { // implements Publication

        // Private attributes.
        var isbn;
        var title;
        var author;

        //Privileged methods.
        this.getIsbn =function () {
            return isbn;
        };
        this.setIsbn = function(newIsbn) {
            if (!checkIsbn(newIsbn)) {
                throw new Error('Book: Invalid ISBN.');
                isbn = newIsbn;
            };
        }
        this.getTitle = function () {
            return title;
        };
        this.setTitle = function (newTitle) {
            title = newTitle || 'No title specified';
        };
        this.getAuthor = function () {
            return author;
        };
        this.setAuthor = function (newAuthor) {
            title = newAuthor || 'No title specified';
        };

        // Constructor code.
        numOfBooks++; // Keep track of how many Books have been instantiated with the private static attribute.
        if (numOfBooks > 50) {
            throw new Error('Book: Only 50 instances of Book can be created.');
        }

        this.setIsbn(newIsbn);
        this.setTitle(newTitle);
        this.setAuthor(newAuthor);
    }
})();

// Public static method.
Book.convertToTitleCase = function(inputString) {
    ...
};

// Public, non-privileged methods.
Book.prototype ={
    display: function () {
        ...
    }
};

与前一节建立的类大题类似,然则有重要辨别.这里的私有成员和特权要领依然声明在组织器中(离别运用 var 和 this 关键字),然则谁人组织器却从本来的一般函数办成了一个内嵌函数,而且被作为包括它的函数的返回值赋给变量 Book.这里建立了一个闭包,内里声清楚明了静态的私有成员.位于外层函数声明今后的一对空括号很重要 — 代码一载入就马上实行这个函数(而不是在挪用 Book 组织函数时),这个函数的返回值是另一个函数,被赋值给 Book 变量 — 一个组织函数,在实例化 Book 时,所挪用的是这个内层函数,外层函数只是用于建立一个可以用来寄存静态私有成员的闭包.
私有的静态成员可以从组织器内部接见,这意味着一切私有函数和特权函数都能接见它们.与其他要领比拟,他们在内存中只会寄存一份.
由于它们在组织器以外,所以不是特权要领,不能接见任何定义在组织器中的私有属性.定义在组织器中的私有要领可以挪用那些私有静态要领.
要推断一个私有要领是不是应当被设想成静态要领,重要看它是不是须要接见任何实例数据,假如不须要那末设想成静态要领更省内存.

常量

在 js 中,可以经由过程建立只需取值器而没有赋值器的私有变量来模拟常量,而且不因对象实例的差别而变化,所以将其作为私有静态属性来设想是合乎情理的.
假定 Class 对象有一个 UPPER_BOUND的常量,那末为了猎取这个常量而举行的要领挪用
Class.getUPPER_BOUND();
为了完成这个取值器,须要运用特权静态要领:

var Class = (function() {

    // Constants (created as private static attributes).
    var UPPER_BOUND = 100;

    // Constructor.
    var ctor = function (constructorArgument) {
        ...
    };

    // Privileged static method.
    ctor.getUPPER_BOUND = function () {
        return UPPER_BOUND;
    };

    // Return the constructor.
    return ctor;

})();

假如须要很多常量,可以建立一个通用的取值器要领,如许就没必要为每一个常量都建立取值器要领:

var Class = (function () {

    // Private static attributes.
    var constants = {
        UPPER_BOUND: 100,
        LOWER_BOUND: -100
    };

    // Constructor.
    var ctor = function(constructorArgument) {
        ...
    };

    // Privileged static method.
    ctor.getConstant = function(name) {
        return constants[name];
    }

    ...
    // Return the constructor.
    return ctor;

})();

单体和对象工场

这两个情势就是运用闭包来建立受庇护的变量空间.后面会逐步触及,在此先扼要引见一下,单体情势运用一个外层函数返回的对象字面量来公然特权成员,而私用成员则被庇护性地封装在外层函数的作用域中,重要道理是:外层函数在定义后马上实行,其效果被赋给一个变量.前面的例子中外层函数返回的都是一个函数,而单体情势中外层函数返回的是一个对象字面量.
对象工场可以运用闭包来建立具有私有成员的对象.最简情势是一个类组织器.

封装的优点

庇护了内部数据的完整性,经由过程讲数据的接见门路限制为取值器和赋值器这两个要领,可以获得对取值和赋值的完整掌握.如许可以削减其他函数所须要的毛病搜检代码数目,而且确保数据不会处于无效状况.别的,对象的重构可以变得更轻松.由于用户不知道对象的内部细节,所以可以为所欲为的修正对象内部运用的数据结构和算法.
经由过程只公然那些在接口中划定的要领,可以弱化模块间的耦合.尽量的进步对象的独立性可以带来很多优点:进步对象的可重用性,使其在必要的时刻可以被替代.运用私有变量可以防备空间争执,假如一个变量在代码中其他处所都不能被接见,就不必忧郁它是不是与顺序中其他处所的对象或许函数重名,大幅修改对象的内部细节也不会影响其他代码.

害处

  • 封装也存在肯定的缺憾.比方,私有要领很难举行单元测试.由于他们另有其内部变量都是私有的,在对象外部没法接见.
    要么经由过程运用公有要领来供应接见门路(如许就市区很多私有要领的优点),要么想法在对象内部定义并实行一切单元测试.最好的解决方法是只对公有方法举行单测.可以掩盖到一切私有要领,然则倒是间接的.这类题目不是 js 特有的,只对公有要领举行单测较易接收

  • 作用域链庞杂的话可以会使毛病调试越发难题,有时刻很难辨别来自差别作用域的很多同名变量.此题目不是经由封装的对象所特有的,然则完成私有要领和属性一切的闭包会让它变得越发庞杂.

  • 过分封装可以会损伤类的灵活性,不利于和小伙伴之间的协作,他可以对你的类的需求相识的并不透辟.

-最大的题目在于 js 完成封装较为难题.不利于新手运用.js 与大多数面向对象言语差别,封装触及的挪用链和定义后马上实行的匿名函数等观点加大了进修难度.

小结

  1. 本文议论了信息隐蔽的观点以及怎样运用封装这类手腕来完成它,js 没有对封装供应内置的支撑,所以须要依靠其他东西.

  2. 假如可以确信其他小伙伴只会运用接口中划定的要领,或许并不是迫切须要坚持内部数据的完整性,那,那末可以运用流派大开型对象.

  3. 定名范例用来示知小伙伴哪些要领是不宜直接要领的内部要领.假如须要真正的私有成员,那末只能运用闭包.经由过程建立一个受庇护的变量空间,可以完成公有,私有和特权成员,静态成员,常量.明白 js 作用域的特性,可以模拟出种种面向对象手艺.

———-剧终———-
    原文作者:南赐
    原文地址: https://segmentfault.com/a/1190000006783722
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞