图片 5

JavaScript设计模式深入分析(1)

最近,我开发一个项目 Angular Cloud Data
Connector,
帮助Angular开发者使用云数据,特别是 Azure移动服务,
使用WEB标准,像索引数据库(indexed
DB)。我尝试建立一种方式,使得JavaScript开发者能将私有成员嵌入到一个对象中。

一直都在考虑这个月分享大家什么东西最好,原计划是打算写一些HTML5中JS方面的内容或者是AJAX方面的,可是自己由于表达能力,时间,还有个人工作方面的问题,这个还是等下个月再做分享吧。

One Day One Tip 之 闭包

时间:2016-08-30 13:39:33
作者:zhongxia

我解决这个问题的技术用到了我命名的闭包空间(closure
space)。在这篇入门文章中,我要分享的是如何在你的项目中用它,及它对主流浏览器的性能和内存的影响。

老规矩,开始正文以前先交代自己写这篇文章的目的和一些注意事项

总结:

概念:
闭包:能够读取其他函数内部变量的函数,在JavaScript中,一个函数return它内部的一个函数。

原理:通过引用变量从而阻止该变量被垃圾回收的机制

优:

  1. 封装私有属性和私有方法,加强封装性,可以达到对变量的保护作用。
  2. 更好的组织代码,比如模块化

缺:

  1. 增加了内存的消耗,并且在某些浏览器下,由于垃圾回收机制不同,有可能导致内存溢出
  2. 增加复杂度
  3. 由于闭包内部变量优先级高于外部变量,所以多查找作用域链中的一个层次,就会在一定程度上影响查找速度。

在深入学习前,咱们先说下,你为什么需要用到私有成员(private members),
还有一种替代方式来模拟私有成员。

1.首先本人一直从事前端开发,所以除了JavaScript其他的语言了解不深,所以文章只会以JavaScript语言的角度去论证;

零、所需知识

要理解闭包,首先必须理解 JavaScript 特殊的变量作用域。
变量的作用域无非就是两种: 全局变量局部变量

JavaScript语言的特殊之处,就在于 函数内部可以直接读取全局变量。
不使用 var 声明的变量,则为全局变量。 b = 100;

function fn(){
  var a = b = 1;  
  // ==> var a = window.b = 1;  // a 是局部变量   b 是全局变量
}
fn();
console.log("b = ",b); // 1
console.log("a = ",a); //VM783:1 Uncaught ReferenceError: a is not defined

函数外部无法访问局部变量。 因此在外部,访问 a 变量 报错。 而 b 变量是
全局变量,因此可以访问到。

如果你想点评本文,尽情推(twitter)我:
@deltakosh。

2.其实我个人在项目用过的模式也不多,对模式的概念的理解也没有那么抽象,所以最近在面试中如果面试官问到与模式相关的问题,自己感觉在对答过程中很郁闷,很多东西表达不清楚,于是就找了些相关资料,才会有这篇文章分享;

一、闭包是什么

闭包是概念?
闭包是指某种程序语言中的代码块允许一级函数存在并且在一级函数中所定义的自由变量能不被释放,直到一级函数被释放前,一级函数外也能应用这些未释放的自由变量。

我的理解就是:闭包就是能够读取其他函数内部变量的函数
由于在JavaScript语言中,只有函数内部的子函数才能读取局部变量,因此可以把闭包简单的理解成
定义在一个函数内部的函数
本质上: 闭包就是将函数内部和外部链接起来的一座桥梁

//eg: example
function a(x){
  var tmp = 10;
  return function(y){
     return (x+y)+(++tmp);
  }
}
var b = a(10);
b(5);  //26, 每执行一次 tmp 加 1

因为 a() 执行后,返回 的 方法 b, 内部引用了 tmp 变量, 导致 tmp
变量的标记+1, 垃圾回收机制就不会清除 tmp这个变量。
然后外部就可以继续访问到 tmp 变量。

1. 为何要用私有成员(Private Members)

当你用JavaScript 创建一个对象时,可以声明值成员(value members)。
如果你打算控制对它们的读/写访问操作,可以如下声明:

var entity = {};

entity._property = "hello world";
Object.defineProperty(entity, "property", {
    get: function () { return this._property; },
    set: function (value) {
        this._property = value;
    },
    enumerable: true,
    configurable: true
});

这样实现,你能完全控制读和写操作。问题在于_property
成员仍然可以直接访问和修改。

这也就是为何我们需要更加稳定可靠的方式,声明私有成员,它智能通过对象的方法来访问。

3.JavaScript模式与前端的工作和成长密不可分,因为这确实不是一个简单的话题,所以我只能尽力用简单表达和例子阐明,而且园子里有很多的高手,所以希望大家踊跃发言(由于水平有限,请大家多多指教,希望嘴下留情);

二、闭包的作用

  1. 读取函数内部的变量
  2. 让内部的变量始终保持在内存中
  3. 设计私有方法和变量【封装框架的时候更明显,典型的如Jquery】

var jQuery = (function(){  

    var jQuery = function(){  
    //TODO  
    }  
    return (window.$ = window.jQuery = jQuery);       
});  

2. 使用闭包空间(Closure Space)

解决方法是使用闭包空间。每当内部函数 (inner fanction)
访问来自外部函数作用域的变量时,浏览器为你分配一段内存空间。有时很取巧,不过就我们的题目来讲,这算是一个完美的解决方案。

我们在上个代码版本中添加这个特性:
var createProperty = function (obj, prop, currentValue) 
{
    Object.defineProperty(obj, prop, 
    {
            get: function () { return currentValue; },
            set: function (value) {
            currentValue = value;
                    },
                    enumerable: true,
                    configurable: true    });
                    } 
var entity = {}; 
var myVar = "hello world";createProperty(entity, "property", myVar);

示例中,createProperty 函数有一个 currentValue 变量,存在 get 和 set
方法。此变量会保存到 get 和 set
函数的闭包空间中。现在,只有这两个函数能看到和更新 currentValue 变量!
任务完成!

唯一需要警惕 caveat,警告,注意)的是源值 (myVar)
仍可访问。下面给出另一个更健壮的版本(保护 myVar 变量):

var createProperty = function (obj, prop) {
    var currentValue = obj[prop];
    Object.defineProperty(obj, prop, {
        get: function () { return currentValue; },
        set: function (value) {
            currentValue = value;
        },
        enumerable: true,
        configurable: true
    });
}

var entity = {
    property: "hello world"
};

createProperty(entity, "property");

采用该函数,
即便源值都销毁(destructed,注:意思是不能直接赋值)了。到此大功告成了!

4.由于这篇文章更多的只是想起到一个介绍和讲解的作用,并不打算对每种模式进行细致的分析,所以每种模式只用到一个至二个例子,可能会造成这个例子的表达并不是最优的或者不够全面,如果各位看官觉得不过瘾,可以再去查找相关资料;

三、闭包的优缺点

3. 性能考虑Performance Considerations

现在咱们看看性能。

很明显,比起一个简单的变量,闭包空间,甚或(对象)属性要慢的多,且更消耗资源。这就是本文更多关注普通方式和闭包空间机制差异的原因。

为证明闭包空间机制并不比标准方式更消耗资源, 我写了下面代码做个基准测试:

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
    <title></title>
</head>
<style>
    html {
        font-family: "Helvetica Neue", Helvetica;
    }
</style>
<body>
    <div id="results">Computing...</div>
    <script>
        var results = document.getElementById("results");
        var sampleSize = 1000000;
        var opCounts = 1000000;

        var entities = [];

        setTimeout(function () {
            // Creating entities
            for (var index = 0; index < sampleSize; index++) {
                entities.push({
                    property: "hello world (" + index + ")"
                });
            }

            // Random reads
            var start = new Date().getTime();
            for (index = 0; index < opCounts; index++) {
                var position = Math.floor(Math.random() * entities.length);
                var temp = entities[position].property;
            }
            var end = new Date().getTime();

            results.innerHTML = "<strong>Results:</strong><br>Using member access: <strong>" + (end - start) + "</strong> ms";
        }, 0);

        setTimeout(function () {
            // Closure space =======================================
            var createProperty = function (obj, prop, currentValue) {
                Object.defineProperty(obj, prop, {
                    get: function () { return currentValue; },
                    set: function (value) {
                        currentValue = value;
                    },
                    enumerable: true,
                    configurable: true
                });
            }
            // Adding property and using closure space to save private value
            for (var index = 0; index < sampleSize; index++) {
                var entity = entities[index];

                var currentValue = entity.property;
                createProperty(entity, "property", currentValue);
            }

            // Random reads
            var start = new Date().getTime();
            for (index = 0; index < opCounts; index++) {
                var position = Math.floor(Math.random() * entities.length);
                var temp = entities[position].property;
            }
            var end = new Date().getTime();

            results.innerHTML += "<br>Using closure space: <strong>" + (end - start) + "</strong> ms";
        }, 0);

        setTimeout(function () {
            // Using local member =======================================
            // Adding property and using local member to save private value
            for (var index = 0; index < sampleSize; index++) {
                var entity = entities[index];

                entity._property = entity.property;
                Object.defineProperty(entity, "property", {
                    get: function () { return this._property; },
                    set: function (value) {
                        this._property = value;
                    },
                    enumerable: true,
                    configurable: true
                });
            }

            // Random reads
            var start = new Date().getTime();
            for (index = 0; index < opCounts; index++) {
                var position = Math.floor(Math.random() * entities.length);
                var temp = entities[position].property;
            }
            var end = new Date().getTime();

            results.innerHTML += "<br>Using local member: <strong>" + (end - start) + "</strong> ms";
        }, 0);

    </script>
</body>
</html>

我创建了一百万个对象,都有属性成员。要完成下面三个测试:

  • 执行 1百万次随机访问属性。
  • 执行1百万次随机访问闭包空间实现版本。
  • 执行1百万次随机访问常规get/set实现版本。

测试结果参见下面表格和图表:

图片 1图片 2

我们发现,闭包空间实现总是快于常规实现,根据浏览器的不同,还可以做进一步的性能优化。

Chrome 上的性能表现低于预期。或许存在 bug,因此,为确认(存在
bug),我联系了 Google 项目组,描述发生的症状。还有,如果你打算测试在
Microsoft
Edge
—微软新发布的浏览器,在windows10
中默认安装—中的性能表现,你可以点击下载 。

然而,如果仔细研究,你会发现,使用闭包空间或属性比直接访问变量成员要10倍左右。 因此,使用要恰当且谨慎。

图片 3

5.做任何事都需要坚持,写博客也是一样,嘿嘿,每月至少一篇(文章确实较长,希望能对朋友们有所帮助,重点部分在前言中有介绍,大家可以选择感兴趣的模式进行深入)。

优点

  1. 延长作用域链。
  2. 更好的组织代码,比如模块化,异步代码转同步等。
  3. 加强封装性,可以打到对变量的保护(第二点的加强)
  4. 处理异步造成的变量不能即时传递的问题

4. 内存占用(Memory Footprint)

我们也得验证该技术不会消耗过多内存。为测试内存占用基准情况,我写了下面代码段:

6.欢迎转载,不过请注明出处,谢谢。

缺点

  1. 由于闭包会使得函数中的变量都被保存在内存中,内存消耗很大,所以不能滥用闭包,否则会造成网页的性能问题,在IE中可能导致内存泄露。解决方法是,在退出函数之前,将不使用的局部变量全部删除
  2. 增加内存的消耗
  3. IE浏览器上 因为回收机制,有内存溢出的风险
  4. 增加了代码复杂度

直接属性引用版本(Reference Code)

var sampleSize = 1000000;
 var entities = []; 
// Creating entities
for (var index = 0; index < sampleSize; index++) {
    entities.push({
            property: "hello world (" + index + ")"
});}

了解JavaScript设计模式我们需要知道的一些必要知识点:(内容相对基础,高手请跳过)

四、闭包用法实战

常规方式版本(Regular Way,get/set)

var sampleSize = 1000000;

var entities = [];

// Adding property and using local member to save private value
for (var index = 0; index < sampleSize; index++) {
    var entity = {};

    entity._property = "hello world (" + index + ")";
    Object.defineProperty(entity, "property", {
        get: function () { return this._property; },
        set: function (value) {
            this._property = value;
        },
        enumerable: true,
        configurable: true
    });

    entities.push(entity);
}

JavaScript闭包

1. 对fun的计算方式进行定制

实际使用的时候,闭包可以创建出非常优雅的设计,允许对funarg上定义的多种计算方式进行定制。如下就是数组排序的例子,它接受一个排序条件函数作为参数:

[1, 2, 3].sort(function (a, b) {
  ... // 排序条件
});

同样的例子还有,数组的map方法是根据函数中定义的条件将原数组映射到一个新的数组中:

[1, 2, 3].map(function (element) {
  return element * 2;
}); // [2, 4, 6]

闭包空间版本(Closure Space Version)

var sampleSize = 1000000;

var entities = [];

var createProperty = function (obj, prop, currentValue) {
    Object.defineProperty(obj, prop, {
        get: function () { return currentValue; },
        set: function (value) {
            currentValue = value;
        },
        enumerable: true,
        configurable: true
    });
}

// Adding property and using closure space to save private value
for (var index = 0; index < sampleSize; index++) {
    var entity = {};

    var currentValue = "hello world (" + index + ")";
    createProperty(entity, "property", currentValue);

    entities.push(entity);
}

之后,我(在三个主流浏览器上)运行所有的三段代码,启动(浏览器)内嵌的内存性能分析器(本示例中使用
F12 工具条):

图片 4

我计算机上运行的结果如下图表:

图片 5

就闭包空间和常规方式,只有 Chrome上,闭包空间(内存占用)表现稍好,在
IE11 和
Firefox上占用内存反而增多,但是浏览器的比较结果e—对于现代浏览器,用户很可能不会在意这点差别。

1.闭包最常用的方式就是返回一个内联函数(何为内联函数?就是在函数内部声明的函数);

2. 函数式参数

使用函数式参数,可以很方便的实现一个搜索方法,并且可以支持无限制的搜索条件:

someCollection.find(function (element) {
  return element.someProperty == 'searchCondition';
});

还有应用函数,比如常见的forEach方法,将函数应用到每个数组元素:

[1, 2, 3].forEach(function (element) {
  if (element % 2 != 0) {
    alert(element);
  }
}); // 1, 3

顺便提下,函数对象的 apply 和
call方法,在函数式编程中也可以用作应用函数。
apply和call已经在讨论“this”的时候介绍过了;这里,我们将它们看作是应用函数
—— 应用到参数中的函数(在apply中是参数列表,在call中是独立的参数):

(function () {
  alert([].join.call(arguments, ';')); // 1;2;3
}).apply(this, [1, 2, 3]);

更多 JavaScript 实践

或许你会吃惊,微软提供了一批有关开源 Javascript
主题的免费学习材料, 我们正在发起一个任务,关于创建更多 Microsoft Edge
来临
系列。 查看我的文章:

  • 基于HTML5

    Babylon.JS
    开发 WebGL 3D 基础
  • 构建单页面应用,基于 ASP.NET 和
    AngularJS
  • HTML
    高级图像技术

或者我们团队系列:

  • HTML/JavaScript
    性能优化使用技巧 (该系列有7部分,从响应式设计到休闲游戏的性能优化)
  • 现代 Web
    平台快速起步
    ( HTML, CSS, and JS基础)
  • 开发通用的 Windows Apps,使用 HTML 和 JavaScript
    快速起步
    (使用你自己的JS构建app)

以及一些免费工具:Visual Studio
社区,Azure
试用版和跨浏览器测试工具用于
Mac, Linux, 或者 Windows。

2.在JavaScript中有作用域和执行环境的问题,在函数内部的变量在函数外部是无法访问的,在函数内部却可以得到全局变量。由于种种原因,我们有时候需要得到函数内部的变量,可是用常规方法是得不到的,这时我们就可以创建一个闭包,用来在外部访问这个变量。

3. 延迟调用

闭包还有另外一个非常重要的应用 —— 延迟调用:

var a = 10;
setTimeout(function () {
  alert(a); // 10, after one second
}, 1000);

结论(Conclusion)

如你所见,对于创建真正的私有数据来讲,闭包空间属性(机制)是一个很棒的做法。或许你得面对内存消耗小幅度增加(问题),但就我的看法,这却很合理 (这个代价可以换取相对于常规方法更高的性能增长)。

随带说一句,
如果你要自己动手试试,所以代码可以在 here下载。 推荐一篇不错的文章, “how-to”
on Azure Mobile Services
here。

3.闭包的用途
主要就是上一点提到的读取函数内部变量,还有一个作用就是可以使这些变量一直保存在内存中。

4. 回调函数

//...
var x = 10;
// only for example
xmlHttpRequestObject.onreadystatechange = function () {
  // 当数据就绪的时候,才会调用;
  // 这里,不论是在哪个上下文中创建
  // 此时变量“x”的值已经存在了
  alert(x); // 10
};
//...

4.使用闭包要注意,由于变量被保存在内存中,所以会对内存造成消耗,所以不能滥用闭包。解决方法是
在退出函数之前,将不使用的局部变量全部删除。

5. 创建封装的作用域来隐藏辅助对象:

var foo = {};

// 初始化
(function (object) {

  var x = 10;

  object.getX = function _getX() {
    return x;
  };

})(foo);

alert(foo.getX()); // 获得闭包 "x" – 10

最后还是上一套闭包的代码吧,这样更直观。

参考文章

  1. 阮一峰-学习Javascript闭包(Closure)
  2. 浅析jQuery核心架构中应用Closure(闭包)的设计模式
  3. 用一道面试题考察对闭包的理解
  4. 深入理解JavaScript系列(16):闭包(Closures)
function f(){   var n = 999;   function f1(){     alert(n+=1);    }   return f1;  }  var result = f();  result(); // 1000  result(); // 1001  result(); // 1002 

封装:通过将一个方法或者属性声明为私用的,可以让对象的实现细节对其他对象保密以降低对象之间的耦合程度,可以保持数据的完整性并对其修改方式加以约束,这样可以是代码更可靠,更易于调试。封装是面向对象的设计的基石。

尽管JavaScript是一门面向对象的语言,可它并不具备将成员声明为公用或私用的任何内部机制,所以我们只能自己想办法实现这种特性。下面还是通过一套完整的代码去分析,介绍什么是私有属性和方法,什么是特权属性和方法,什么是公有属性和方法,什么是公有静态属性和方法。

私有属性和方法:函数有作用域,在函数内用var
关键字声明的变量在外部无法访问,私有属性和方法本质就是你希望在对象外部无法访问的变量。

特权属性和方法:创建属性和方法时使用的this关键字,因为这些方法定义在构造器的作用域中,所以它们可以访问到私有属性和方法;只有那些需要直接访问私有成员的方法才应该被设计为特权方法。

共有属性和方法:直接链在prototype上的属性和方法,不可以访问构造器内的私有成员,可以访问特权成员,子类会继承所有的共有方法。

共有静态属性和方法:最好的理解方式就是把它想象成一个命名空间,实际上相当于把构造器作为命名空间来使用。

 /* -- 封装 -- */  var _packaging = function(){      //私有属性和方法      var name = 'Darren';      var method1 = function(){         //...      }      //特权属性和方法      this.title = 'JavaScript Design Patterns' ;      this.getName = function(){         return name;      }   }   //共有静态属性和方法   _packaging._name = 'Darren code';   _packaging.alertName = function(){      alert(_packaging._name);   }  //共有属性和方法  _packaging.prototype = {      init:function(){         //...      }   } 

发表评论

电子邮件地址不会被公开。 必填项已用*标注