JavaScript的封装

235 阅读10分钟

封面动物

大脑腐~

封装(encapsulation)

前言:为什么单独写了一个关于封装的文章?虽然不是什么大牛,在JS中最好理解、也最常用的就是封装,大概的理念和原则还了解的。但是突然有一天,我需要和别人去解释什么是封装时,我发现我词穷了,大脑空白了。
  • 什么是封装?
  • JS中如何定义是封装?
  • JS封装的各种表现是什么样的?
可以聊的,可以讲的还是蛮多的,所以有了这篇文章。
简介:封装是面向对象的三个基本特征之一,将现实世界的事物抽象成计算机领域中的对象,对象同时具有属性和行为(方法),这种抽象就是封装.

重要特性: 数据隐藏. 对象只对外提供与其它对象交互的必要接口,而将自身的某些属性和实现细节对外隐藏,通过这种方式,对象对内部数据提供了不同级别的保护,以防止程序中无关的部分意外的改变或错误的使用了对象的私有部分。

这样就在确保正常交互的前提下,保证了安全性,不需要关心对象实现的方法即可使用这个对象。这个概念就是“不要告诉我你是怎么做的,只要做就可以了。”

函数(function)--最简单的封装

前言:首先第一个问题,函数是不是一种封装?当然是封装了。《JavaScript高级程序设计》一书中在3.7函数部分开头就说了:
函数对任何语言来说都是一个核心的概念。通过函数可以封装任意多条语句,而且可以在任何地方、任何时候调用执行。
如何封装: 将零散的的语句写进函数的花括号内,成为函数体,然后就可以调用了。
未封装代码:
var body = document.getElementsByTagName("body")[0];
        var h1 = document.createElement("h1");
        body.style.backgroundColor = "green";
        h1.innerText = "绿了";
        body.appendChild(h1);
缺点:
  • 易被同名变量覆盖--因为在全局作用域下声明的变量,容易被同名变量覆盖
  • 立即执行--解析器读取到此处立即执行
封装代码:
function makeGreen() {
            var body = document.getElementsByTagName("body")[0];
            var h1 = document.createElement("h1");
            body.style.backgroundColor = "green";
            h1.innerText = "绿了";
            body.appendChild(h1);
        }
优点:
  • 避免了全局变量--因为存在函数作用域(函数作用域画重点,以后要考)
  • 按需执行--解析器读取到此处,函数并未执行,只有当你需要的时候,调用此函数即可
  • 提高代码重用性
注意事项:
  • 函数存在局部作用域
  • 函数存在函数提升--因此,函数可以在声明之前调用
  • 函数内可以访问作用域链的上游
总结:关于最常用的函数其实不用说太多,主要是函数作为js的封装基础,绕不开它,所以先把它写在了前面。

对象(object)--字面量表示法与命名空间

前言:关于命名空间的介绍
命名空间是用来组织和重用代码的。如同名字一样的意思,NameSpace(名字空间),之所以出来这样一个东西,是因为人类可用的单词数太少,并且不同的人写的程序不可能所有的变量都没有重名现象,对于库来说,这个问题尤其严重,如果两个人写的库文件中出现同名的变量或函数(不可避免),使用起来就有问题了。为了解决这个问题,引入了名字空间这个概念,通过使用 namespace xxx;你所使用的库函数或变量就是在该名字空间中定义的,这样一来就不会引起不必要的冲突了。
优点:避免同名的变量或函数名冲突。
缺点:尚不明确。
代码:对象字面量表示法的命名空间
//对象字面量命名空间
        var myNamespace={
            myProp:'我的属性',
            myConf:{
                cache:true,
                lang:'zh'
            },
            //基本方法
            myMethod:function(){
                console.log('输出一句没有用的废话');
            },
            //根据配置信息进行输出
            readConf:function(){
                console.log(this.myConf.cache?'启用了缓存':'禁用了缓存','系统语言为:'+ (this.myConf.lang=="zh"?'中文':'English'));
            }
        }
        myNamespace.myMethod();//输出一句没有用的废话
        myNamespace.readConf();//启用了缓存 系统语言为:中文
高级应用:这里面讲的很透彻了,我就不重复了。
[《深入剖析js命名空间函数namespace - digdeep - 博客园》​](https://www.cnblogs.com/digdeep/p/4175969.html)

对象(object)--关心实例与原型之间联系的封装

前言:js中,对象是某个引用类型的实例,大多数引用类型都是Object类型的实例。对象也可以看做是属性的无序集合,每个属性都是一个名/值对,值可以是原始值或其他对象。
最简单的封装:
假定我们把人看成一个对象,它有"名字"和"性别"两个属性(property),以它作为原型,通过字面量(对象直接量)表示如下:
var Person = {
    name : '',
    sex : ''
  }
现在,我们需要根据这个原型对象的规格(schema),生成两个实例对象。
var man = {}; // 创建一个空对象

        man.name = "亚当"; // 按照原型对象的属性赋值

        man.sex = 1;

        var women = {};

        women.name = "夏娃";

        women.sex = 0;
这就是最简单的封装了,把两个属性封装在一个对象里面。其中封装的是数据。
缺点:
  • 生成几个实例,写起来就非常麻烦,代码重复;而且还是相似的对象
  • 实例与原型之间没有什么联系;
进阶封装-工厂模式:
前言:工厂模式是软件工程领域一种广为人知的设计模式,这种模式抽象(封装)了创建具体对象的过程。js中,用函数来封装以特定接口创建对象的细节。
代码:
function createPerson(name, sex) {
        return {
                name: name,
                sex: sex
        };
}
//或者 变体
function createPerson(name, sex) {
        var o = new Object();
        o.name = name;
        o.sex = sex;
        return o;
}

var adam = createPerson("亚当", 1);
var eve = createPerson("夏娃", 0);
优点:这种封装解决了代码重复的问题;
缺点:adam和eve之间没有内在的联系,不能反映出它们是同一个原型对象的实例;
构造函数式模式:
前言:JS中的构造函数可以用来创建特定类型的对象。也可以创建自定义的构造函数,从而自定义对象类型的属性和方法。
代码:
function Person(name, sex) {
        this.name = name;
        this.sex = sex;
        this.sayName = function () {
                alert(this.name);
        };
}

var adam = new Person("亚当", 1);
var eve = new Person("夏娃", 0);
将上方工厂模式的代码更改为构造函数模式的代码,除了增加了sayName的方法外,还有些区别。
区别:
  • 没有显式的创建对象;
  • 直接将属性和方法赋值给了this对象;
  • 没有return;
注意:要创建Person的实例,必须使用new操作符。
以这种方式调用构造函数实际上会经历如下4个步骤:
  1. 创建一个新对象;
  2. 将构造函数的作用域赋给新对象(因此this就指向了这个新对象);
  3. 执行构造函数中的代码(为这个新对象添加属性);
  4. 返回新对象;
可以通过 instanceof 操作符确认实例是否后属于Person构造函数
代码:
adam instanceof Person;//true
eve instanceof Person;//true

函数--闭包定义常量的封装

前言:利用闭包函数作用域的特点,实现保护常量,达成开闭原则。
代码:
var PI = (function () {
            var _pi = 3.1415926;
            return {
                get: function () {
                    return _pi;
                }
            }
        }())

   console.log(PI.get()); //3.1415926
可以看出,PI通过自调用函数返回一个对象,对象中有一个方法,作用是获取函数内 _pi的值。
想获取到π的值只能通过PI的get方法,并且没有给定set的方法,不能够更改。
高级应用:《JavaScript设计模式》[美]Addy Osmani 中写到的 Module(模块)模式。
简介:模块是任何强大应用程序架构不可或缺的一部分,它通常能够帮助我们清晰的分离和组织项目中的代码单元。在JavaScript中,有几种用于实现模块的方法,包括:
  • 对象字面量表示法
  • Module模式
  • AMD模块
  • CommonJS模块
  • ECMASript Harmony 模块
其中对象字面量表示法见上文,后三个暂不讨论(其实我平时主要用的是AMD,有兴趣的可以先搜索资料了解一下),先说一下Module模式。Module模式最初被定义为一种在传统软件工程中为类提供私有和公有封装的方法。js中Module模式用于进一步的模拟类的概念,通过这种方式,能够使一个单独的对象拥有共有/私有方法和变量,从而屏蔽来自全局作用域的特殊部分。
优点:函数名与页面上其他脚本定义的函数冲突的可能性降低。
代码:
var myNamespace = (function () {
            //私有变量 外部不可访问
            var myProp = 'siyoubianliang';
            //私有函数
            var myMethod = function (txt) {
                console.log('私有函数输出:' + txt);
            };
            return {
                //共有配置
                myConf: {
                    cache: true,
                    lang: 'zh'
                },
                //根据配置信息进行输出
                readConf: function () {
                    // 输出私有变量
                    console.log("输出私有变量" + myProp)
                    //调用私有函数
                    myMethod(this.myConf.cache ? '启用了缓存' : '禁用了缓存');
                }
            }
        })();

        myNamespace.readConf();
        // 输出私有变量siyoubianliang
        //我的属性 私有函数输出:启用了缓存 

        myNamespace.myMethod(); //myNamespace.myMethod is not a function  function 
应用变形:Revealing Module (揭示模块)模式 (require.js就是这么实现的)
代码:
var myNamespace = (function () {
              //私有函数
            var myMethod = function (txt) {
                console.log('私有函数输出:' + txt);
            };
            var myMethod2 = function (txt) {
                console.log('私有函数2输出:' + txt+'尾巴');
            };

            //将暴露的共有指针指向私有函数和属性上
            return {
               simpleLog:myMethod,
               tailLog:myMethod2
            }
        })();
优点:很容从模块代码底部看出那些函数和变量时可以被公开访问的。
缺点:只适用于函数。
总结:要熟练掌握闭包与活动对象的相关知识点,才好理解学习这一块。不然会觉得很吃力。

对象(object)--通过访问器属性的封装

前言:ECMAScript中有两种属性:数据属性 和 访问器属性。这里是利用访问器属性进行的封装,从而达到对值的保护,在下方代码中只提供获取值的接口,不提供设置值的接口。
代码:
var person = {
            name: "杨超越"
        };
        //定义一个只有getter的age属性
        Object.defineProperty(person, 'age', {
            value: new Date().getFullYear() - 1998,
            get function() {
                return this.age;
            }
        })

        console.log(person.age); //21
        person.age = 22;
        console.log(person.age); //21

类式封装

前言:其中利涉及闭包作用域,继承的知识点。
代码:
//实现类,内部通过this指向当前对象,通过this添加属性或者方法
        var Book = function (id, name, price) {

            //私有属性
            var name = '',
                price = 0;
            //私有方法
            function checkId() {

            }
            //对象共有属性
            this.id = id;

            //特权方法---可操作私有属性的方法
            this.getName = function () {};
            this.setName = function () {};
            this.getPrice = function () {};
            this.setPrice = function () {};

            //对象共有方法
            this.copy = function () {
                // ...
            }
            //安全模式
            // console.log(this)//Book or Window
            if(this instanceof Book){

                this.setName(name);
                this.setPrice(price);
            }else{
                return new Book(id, name, price)
            }

        }

        //类静态共有属性和方法(实例对象不能访问)
        Book.isChinses = true;

        Book.resetTime = function () {
            console.log('new time')
        }

        //共有属性和方法
        Book.prototype = {
            isJSBook: false,

            display: function () {
                console.log('display')
            }
        }

总结:

上面许多例子其实并不是传统意义上的封装,而我却都列出来了。因为他们最终都是一种对数据,行为的保护和包装。如果我新的理解或内容,我会再次修改和补充的。

参考资料:

  • 《JavaScript权威指南》
  • 《JavaScript高级程序设计》
  • 《JavaScript设计模式》张容铭
  • 《JavaScript设计模式》[美]Addy Osmani