《JavaScript 设计模式与开发实战》第一部分(1、2、3章)笔记

第1章:面向对象的JavaScript

  1. 动态类型和鸭子类型

    编程语言按照数据类型大体可以分为两类:

    ① 静态类型语言:在编译时便已确定变量的类型。

    ② 动态类型语言:变量类型要到程序运行的时候,待变量被赋予某个值之后,才会具有某种类型。

    【鸭子类型】:如果它走起路来像鸭子,叫起来像鸭子,那么它就是鸭子。

    鸭子类型指导我们只关注对象的行为,而不关注对象本身,也就是关注 HAS-A,而不是 IS-A。

    ☛ 在动态类型语言的面向对象设计中,鸭子类型的概念至关重要。利用鸭子类型的思想,我们不必借助超类型的帮助,就能轻松地在动态类型语言中实现一个原则:“面向接口编程,而不是面向实现编程。

  2. 多态

    【多态的实际含义】:同一操作作用于不同的对象上面,可以产生不同的解释和不同的执行结果。即,给不同的对象发送同一个消息的时候,这些对象会根据这个消息分别给出不同的反馈。

    【多态背后的思想】:是将“做什么”和“谁去做以及怎样去做”分离开来,也就是将“不变的事物”与“可能改变的事物”分离开来。

    静态类型的面向对象语言通常被设计为可以“向上转型”,使用继承得到多态效果,是让对象表现出多态性的最常用手段。

    JavaScript 对象的多态性是与生俱来的,并不需要诸如向上转型之类的技术来取得多态的效果。

    将行为分布在各个对象中,并让这些对象各自负责自己的行为,这正是面型对象设计的优点。

    对象的多态性提示我们,“做什么”和“怎么去做”是可以分开的。

  3. 封装

    封装的目的是将信息隐藏,封装应该被视为“任何形式的封装”,即,封装不仅仅是隐藏数据,还包括隐藏实现细节、设计细节以及隐藏对象的类型等。

    (1)封装数据

    JavaScript 只能依赖变量的作用域来实现封装特性,而且只能模拟出 public 和 private 这两种封装性。

     var myObject = (function() {
         var __name = 'sven'; // 私有(private)变量
         return {
             getName: function() { // 公开(public)函数
                 return __name;
             }
         }
     })();
     
     console.log(myObject.getName()); // 'sven'
     console.log(myObject.__name); // 'undefined'
    

    (2)封装实现

    封装使得对象之间的耦合变松散,对象之间只通过暴露的 API 接口来通信。

    (3)封装类型

    封装类型是静态语言中一种重要的封装方式。一般而言,封装类型是通过抽象类和接口来进行的。

    而 JavaScript 本身是一门类型模糊的语言,在封装类型方面没有能力,也没有必要做的更多。

    (4)封装变化

    从设计模式的角度出发,封装在更重要的层面体现为封装变化。

    通过封装变化的方式,把系统中稳定不变的部分和容易变化的部分隔离开来,在系统的演变过程中,我们只需要替换那些容易变化的部分,如果这些部分是已经封装好的,替换起来也相对容易。

    这可以很大程度地保证程序的稳定性和可扩展性。

  4. 原型模式和基于原型的 JavaScript 对象系统

    (1)使用克隆的原型模式

    原型模式是通过克隆来创建对象的。使用原型模式,我们只需要调用负责克隆的方法,便能完成同样的功能。

    【原型模式的实现关键】:语言本身是否提供了 clone 方法,ECMAScript 5 提供了 Object.create 方法,可以用来克隆对象:

     var Plane = function() {
         this.blood = 100;
         this.attackLevel = 1;
         this.defenseLevel = 1;
     };
     
     var plane = new Plane();
     plane.blood = 500;
     plane.attackLevel = 10;
     plane.defenseLevel = 7;
     
     var clonePlane = Object.create(plane);
     console.log(clonePlane);
     
     // 在不支持 Object.create 方法的浏览器中,可以使用以下代码:
     Object.create = Object.create || function(obj) {
         var F = function() {};
         F.prototype = obj;
     
         return new F();
     }
    

    (2)克隆是创建对象的手段

    原型模式的真正目的并非在于需要得到一个一模一样的对象,而是提供了一种便捷的方式去创建某个类型的对象,克隆只是创建这个对象的过程和手段。

    (3)体验 Io 语言

    原型模式不仅仅是一种设计模式,也是一种编程范型。JavaScript 就是使用原型模式来搭建整个面向对象系统的。

    在 JavaScript 语言中不存在类的概念,对象也并非从类中创建出来的,所有的 JavaScript 对象都是从某个对象上克隆而来的。

    (4)原型编程范型的一些规则

    【原型编程中的一个重要特性】:当对象无法响应某个请求时,会把该请求委托给它自己的原型。

    【原型编程范型至少包括以下基本规则】:

    • 所有的数据都是对象。
    • 要得到一个对象,不是通过实例化类,而是找到一个对象作为原型并克隆它。
    • 对象会记住它的原型。
    • 如果对象无法响应某个请求,它会把这个请求委托给它自己的原型。

    (5)JavaScript 中的原型继承

    ① 所有的数据都是对象

    JavaScript 在设计时,模仿 Java 引入了两套类型机制:基本类型和对象类型。基本类型包括 undefinednumberbooleanstringfunctionobject

    按照 JavaScript 设计者的本意,除了 undefined 之外,一切都应是对象。为了实现这一目标,numberbooleanstring 这几种基本类型数据也可以通过“包装类”的方式变成对象类型数据来处理。

    我们不能说在 JavaScript 中所有的数据都是对象,但可以说绝大部分数据都是对象。 JavaScript 中的跟对象是 Object.prototype 空对象,每个对象都是从它克隆而来的,它就是它们的原型。

     var obj1 = new Object();
     var obj2 = {};
     
     console.log(Object.getPrototypeOf(obj1) === Object.prototype); // true
     console.log(Object.getPrototypeOf(obj2) === Object.prototype); // true
    

    ② 要得到一个对象,不是通过实例化类,而是找到一个对象作为原型并克隆它

    JavaScript 的函数既可以作为普通函数被调用,也可以作为构造器被调用。当使用 new 运算符来调用函数时,此时的函数就是一个构造器。

    ③ 对象会记住它的原型

    就 JavaScript 的真正实现来说,其实并不能说对象有原型,而只能说对象的构造器有原型。对象把请求委托给它的构造器的原型。。

    对象的 __proto__ 的隐藏属性,默认会指向它的构造器的原型对象,即 {Constructor}.prototype。实际上,__proto 就是对象跟“对象构造器的原型”联系起来的纽带。

    ④ 如果对象无法响应某个请求,它会把这个请求委托给它自己的原型

    (6)原型继承的未来

    很多时候,设计模式其实都体现了语言的不足之处。Peter Norvig 曾说,设计模式是对语言不足的补充,如果要使用设计模式,不如去找一门更好的语言。

    虽然大多数主流浏览器都提供了 Object.create 方法,但通过其来创建对象的效率并不高,通常比通过构造函数创建对象要慢。

    另外,通过设置构造器的 prototype 来实现原型继承的时候,除了根对象 Object.prototype 本身之外,任何对象都会有一个原型。而通过 Object.object(null) 可以创建出没有原型的对象。

    ECMAScript 6 带来了新的 Class 语法。但其背后仍是通过原型机制来创建对象的。示例代码:

     class Animal {
         constructor(name) {
             this.name = name;
         }
     
         getName() {
             return this.name;
         }
     }
     
     class Dog extends Animal {
         constructor(name) {
             super(name);
         }
     
         speak() {
             return "woof";
         }
     }
     
     var dog = new Dog('Scamp');
     console.log(dog.getName() + ' says ' + dog.speak());
    

第2章:this、call 和 apply

  1. this

    在 JavaScript 中,this 总是指向一个对象,而具体指向哪一个对象是在运行时基于函数的执行环境动态绑定的,而非函数被声明时的环境。

    this 的指向大致可以分为以下4种:

    • 作为对象的方法调用。
    • 作为普通函数调用。
    • 构造器调用。
    • Function.prototype.callFunction.prototype.apply 调用

    ☛ 丢失的 this

     var obj = {
         myName: 'sven',
         getName: function() {
             return this.myName;
         }
     };
     
     // 作为对象的方法调用
     console.log(obj.getName()); // 'sven'
     
     // 作为普通函数调用
     var getName2 = obj.getName;
     console.log(getName2()); // 'undefined'
    
  2. call 和 apply

    【区别】:传参方式不同

    【用途】:

    (1)改变 this 的指向

    (2)Function.prototype.bind

    // 简化版实现

     Function.prototype.bind = function(context) {
         var self = this;
         return function() {
             return self.apply(context, arguments);
         }
     };
     
     var obj = {
         name: 'sven'
     };
     
     var func = function() {
         alert(this.name);
     }.bind(obj);
     
     func();
    

    // 完整版实现

     Function.prototype.bind = function() {
         var self = this, // 保存原函数
             context = [].shift.call(arguments), // 需要绑定的 this 上下文
             args = [].slice.call(arguments); // 剩余的参数转成数组
     
         return function() {
             return self.apply(context, [].concat.call(args, [].slice.call(arguments)));
             // 执行新的函数的时候,会把之前传入的 context 当做函数体内的 this
             // 并且组合两次分别传入的参数,作为新函数的参数
         }
     };
     
     var obj = {
         name: 'sven'
     };
     
     var func = function(a, b, c, d) {
         alert(this.name); // 输出:sven
         alert([a, b, c, d]); // 输出: [1, 2, 3, 4]
     }.bind(obj, 1, 2);
     
     func(3, 4);
    

    (3)借用其他对象的方法

    // 方法1

     var A = function(name) {
         this.name = name;
     };
     
     var B = function() {
         A.apply(this, arguments);
     };
     
     B.prototype.getName = function() {
         return this.name;
     };
     
     var b = new B('sven');
     console.log(b.getName()); // 输出:sven
    

    // 方法2:借用 Array.prototype 对象操作 arguments

     Array.prototype.slice 	// 转成真正的数组
     Array.prototype.shift	// 截去 arguments 列表中的头一个元素
     Array.prototype.push	// 往 arguments 中添加一个新元素
    

第3章:闭包和高阶函数

在 JavaScript 版本的设计模式中,许多模式都可以用闭包和高阶函数来实现。

  1. 闭包

    闭包的形成与变量的作用域以及变量的生存周期密切相关。

    (1)变量的作用域:

    就是指变量的有效范围,最常指函数中声明的变量作用域。

    (2)变量的生存周期:

    ① 全局变量:生存周期是永久的,除非主动销毁这个全局变量。

    ② 局部变量:当退出函数时,局部变量即失去它们的价值,会随着函数调用的结束而被销毁。

    ★ 闭包的经典应用:

     <div>1</div>
     <div>2</div>
     <div>3</div>
     <div>4</div>
     <div>5</div>
    
     <script>
         var nodes = document.getElementsByTagName('div');
    
         // 无论点击哪个div,结果都是5
         // 因为div节点的onclick事件是被异步触发的,
         // 当事件被触发时,for循环早已结束,此时变量i的值已经是5
         // 所以在div的onclick事件函数中顺着作用域链从内到外查找变量i时,查找到的值总是5
         for (var i = 0, len = nodes.length; i < len; i++) {
             nodes[i].onclick = function() {
                 console.log(i);
             }
         }
     </script>
    

    ☞ 解决方法:

     for (var i = 0, len = nodes.length; i < len; i++) {
         (function(i) {
             nodes[i].onclick = function() {
                 console.log(i);
             }
         })(i);
     }
    

    在闭包的帮助下,把每次循环的 i 值都封闭起来。当在事件函数中顺着作用域链从内到外查找变量 i 时,会先找到被封闭在闭包环境中的 i

    (3)闭包的更多作用

    ① 封装变量

     /*var mult = function() {
         var a = 1;
         for (var i = 0, len = arguments.length; i < len; i++) {
             a = a * arguments[i];
         }
         return a;
     };*/
     
     // 改进1:
     // 对于那些相同的参数来说,每次都进行计算是一种浪费
     // 加入缓存机制来提高函数的性能
     
     /*var cache = {};
     var mult = function() {
         var args = Array.prototype.join.call(arguments, ',');
         if (cache[args]) {
             return cache[args];
         }
     
         var a = 1;
         for (var i = 0, len = arguments.length; i < len; i++) {
             a = a * arguments[i];
         }
     
         return cache[args] = a;
     }*/
     
     // 继续改进2:减少页面中的全局变量
     /*var mult = (function() {
         var cache = {};
         return function() {
             var args = Array.prototype.join.call(arguments, ',');
             if (cache[args]) {
                 return cache[args];
             }
     
             var a = 1;
             for (var i = 0, len = arguments.length; i < len; i++) {
                 a = a * arguments[i];
             }
     
             return cache[args] = a;
         }
     })();*/
     
     // 继续改进4:提炼函数是代码重构中的一种常见技巧
     var mult = (function() {
         var cache = {};
         var calculate = function() {
             var a = 1;
             for (var i = 0, len = arguments.length; i < len; i++) {
                 a = a * arguments[i];
             }
             return a;
         };
     
         return function() {
             var args = Array.prototype.join.call(arguments, ',');
     
             if (args in cache) {
                 return cache[args];
             }
     
             return cache[args] = calculate.apply(null, arguments);
         }
     })();
     
     console.log(mult(1, 2, 3, 4));
    

    ② 延续局部变量的寿命

     // 把 img 变量用闭包封闭起来,解决请求丢失的问题
     var report = (function() {
         var imgs = [];
         return function(src) {
             var img = new Image();
             imgs.push(img);
             img.src = src;
         }
     })();
    

    (4)闭包和面向对象设计

    对象以方法的形式包含了过程,而闭包则是在过程中以环境的形式包含了数据。通常用面向对象能实现的功能,用闭包也能实现。反之亦然。

     // 闭包实现
     var extent = function() {
         var value = 0;
         return {
             call: function() {
                 value++;
                 console.log(value);
             }
         }
     };
     
     var extent = extent();
     extent.call(); // 1
     extent.call(); // 2
     
     // 面向对象写法1
     var extent = {
         value: 0,
         call: function() {
             this.value++;
             console.log(this.value);
         }
     };
     
     extent.call(); // 1
     extent.call(); // 2
     
     // 面向对象写法2
     var Extent = function() {
         this.value = 0;
     };
     
     Extent.prototype.call = function() {
         this.value++;
         console.log(this.value);
     }
     
     var extent = new Extent();
     extent.call(); // 1
     extent.call(); // 2
    

    (5)用闭包实现命令模式

    在面向对象版本的命令模式中,预先植入的命令接收者被当成对象的属性保存起来;而在闭包版本的命令模式中,命令接收者会被封闭在闭包形成的环境中。

    (6)闭包与内存管理

    • 局部变量本来应该在函数退出的时候被解除引用,但如果局部变量被封闭在闭包形成的环境中,那么这个局部变量就能一直生存下去。从这个意义上,闭包的确会使一些数据无法被及时销毁。

    • 使用闭包的一部分原因是我们选择主动把一些变量封闭在闭包中,因为可能在以后还需要使用这些变量,把这些变量方闭包中和放在全局环境中,对内存方面的影响是一致的,这里并不能说是内存泄漏。

    • 如果将来要回收这些变量,可以手动把这些变量设为 null。

    • 跟闭包和内存泄漏有关系的地方是,使用闭包的同时比较容易形成循环引用,如果闭包的作用域链中保存着一些DOM节点,这时候可能造成内存泄漏。但这本质上并非由闭包造成的。

    • 同样,如果要解决循环引用带来的内存泄漏问题,我们只需要把循环引用中的变量设为 null 即可。

  2. 高阶函数

    高阶函数是指至少满足下列条件之一的函数:

    • 函数可以作为参数被传递;
    • 函数可以作为返回值输出。

    (1)函数作为参数传递

    ① 回调函数

    回调函数的应用不仅只在异步请求中,当一个函数不适合执行一些请求时,我们也可以把这些请求封装成一个函数,并把它作为参数传递给另外一个函数,“委托”给另外一个函数来执行。

     var appendDiv = function(callback) {
         for (var i = 0; i < 100; i++) {
             var div = document.createElement('div');
             div.innerHTML = 1;
             document.body.appendChild(div);
             if (typeof callback === 'function') {
                 callback(div);
             }
         }
     };
     
     appendDiv(function(node) {
         node.style.display = 'none';
     });
    

    ② Array.prototype.sort

    把用什么规则去排序(可变的)的部分封装在函数参数里,动态传入。

     // 从小到大排序
     [1, 4, 3].sort(function(a, b) {
     	return a - b;
     });
    

    (2)函数作为返回值输出

    ① 判断数据的类型

     // 判断数据类型1
     var isType = function(type) {
         return function(obj) {
             return Object.prototype.toString.call(obj) === '[object ' + type + ']';
         }
     };
     
     var isString = isType('String');
     var isArray = isType('Array');
     var isNumber = isType('Number');
     
     console.log(isArray([1, 2, 3]));
     
     // 判断数据类型2:循环语句,批量注册这些 isType 函数
     var Type = {};
     
     for (var i = 0, type; type = ['String', 'Array', 'Number'][i++];) {
         (function(type) {
             Type['is' + type] = function(obj) {
                 return Object.prototype.toString.call(obj) === '[object ' + type + ']';
             }
         })(type);
     }
     
     Type.isArray([]);
     Type.isString('str');
    

    ② getSingle

     // 既把函数当作参数传递,又让函数执行后返回了另外一个函数
     var getSingle = function(fn) {
         var ret;
         return function() {
             return ret || (ret = fn.apply(this, arguments));
         };
     };
     
     var getScript = getSingle(function() {
         return document.createElement('script');
     });
     
     var script1 = getScript();
     var script2 = getScript();
     
     alert(script1 === script2); // true
    

    (3) 高阶函数实现 AOP

    AOP(面向切面编程)的主要作用是把一些跟核心业务逻辑模块无关的功能抽离出来,这些跟业务逻辑无关的功能通常包括日志统计、安全控制、异常处理等。

    好处:(1)可以保持业务逻辑模块的纯净和高内聚性;(2)可以很方便地服用日志统计等功能模块。

    通常在 JavaScript 中实现 AOP,都是指把一个函数“动态织入”到另外一个函数之中。

     Function.prototype.before = function(beforefn) {
         var __self = this;
         return function() {
             beforefn.apply(this, arguments);
             return __self.apply(this, arguments);
         }
     };
     
     Function.prototype.after = function(afterfn) {
         var __self = this;
         return function() {
             var ret = __self.apply(this, arguments);
             afterfn.apply(this, arguments);
             return ret;
         }
     }
     
     var func = function() {
         console.log(2);
     };
     
     func = func.before(function() {
         console.log(1);
     }).after(function() {
         console.log(3);
     });
     
     func();
    

    (4) 高阶函数的其他应用

    ① currying

    currying 又称部分求值。一个 currying 的函数首先会接受一些参数,接受了这些参数之后,该函数并不会立即求值,而是继续返回另外一个函数,刚才传入的参数在函数形成的闭包中被保存起来。待到函数被真正需要求值的时候,之前传入的所有参数都会被一次性用于求值。

     var currying = function(fn) {
         var args = [];
     
         return function() {
             if (arguments.length === 0) {
                 return fn.apply(this, args);
             } else {
                 [].push.apply(args, arguments);
                 return arguments.callee;
             }
         }
     };
     
     var cost = (function() {
         var money = 0;
     
         return function() {
             for (var i = 0, len = arguments.length; i < len; i++) {
                 money += arguments[i];
             }
             return money;
         }
     })();
     
     var cost = currying(cost); // 转化成curring函数
     cost(100);
     cost(200);
     cost(300);
     
     cost(); // 600
    

    ② uncurring

    在 JavaScript 中,当我们调用对象的某个方法时,其实不用去关心该对象原本是否被设计为拥有这个方法,这是动态类型语言的特点(鸭子类型思想)。

    同理,一个对象未必只能使用它自身的方法,可以让它去借用一个原本不属于它的方法。

     // 实现1
     Function.prototype.uncurrying = function() {
         var self = this;
         return function() {
             var obj = Array.prototype.shift.call(arguments);
             return self.apply(obj, arguments);
         };
     };
     
     var push = Array.prototype.push.uncurrying();
     var obj = {
         'length': 1,
         '0': 1
     };
     
     push(obj, 2);
     console.log(obj);
     
     // 实现2
     Function.prototype.uncurrying = function() {
         var self = this;
         return function() {
             return Function.prototype.call.apply(arguments);
         };
     };
    

    ③ 函数节流

    【函数被频繁调用的场景】:

    • window.onresize 事件
    • mousemove 事件
    • 上传进度

    【实现代码】:

     // 原理:将即将被执行的函数用 setTimeout 延迟一段时间执行。
     // 如果该次延迟执行还没有完成,则忽略接下来调用该函数的请求。
     var throttle = function(fn, interval) {
         var __self = fn, // 保存需要被延迟执行的函数引用
             timer, // 定时器
             firstTime = true; // 是否是第一次调用
     
         return function() {
             var args = arguments,
                 __me = this;
     
             if (firstTime) { // 如果是第一次调用,不需要延迟执行
                 __self.apply(__me, args);
                 return firstTime = false;
             }
     
             if (timer) { // 如果定时器还在,说明前一次延迟执行还没有完成
                 return false;
             }
     
             timer = setTimeout(function() {
                 clearTimeout(timer);
                 timer = null;
                 __self.apply(__me, args);
             }, interval || 500);
         };
     };
    

    ④ 分时函数

     // 原理:让创建节点的工作分批进行
     // 比如把1秒钟创建1000个节点,改为每隔200毫秒创建8个节点
     var timeChunk = function(arr, fn, count) {
         var obj,
             t;
     
         var len = arr.length;
     
         var start = function() {
             for (var i = 0; i < Math.min(count || 1, arr.length); i++) {
                 var obj = arr.shift();
                 fn(obj);
             }
         };
     
         return function() {
             t = setInterval(function() {
                 if (arr.length === 0) { // 如果全部节点都已经被创建好
                     return clearInterval(t);
                 }
                 start();
             }, 200); // 分批执行的时间间隔,也可以用参数的形式传入
         };
     };
    

    ⑤ 惰性加载函数

     // 原理:在第一次进入条件分支之后,在函数内部会重写这个函数
     // 重写之后就是我们期望的addEvent函数
     // 在下一次进入addEvent函数的时候,addEvent函数里不再存在条件分支语句
     var addEvent = function(elem, type, handler) {
         if (window.addEventListener) {
             addEvent = function(elem, type, handler) {
                 elem.addEventListener(type, handler, false);
             }
         } else if (window.attachEvent) {
             addEvent = function(elem, type, handler) {
                 elem.attachEvent('on' + type, handler);
             }
         }
     
         addEvent(elem, type, handler);
     };
    
  3. 小结

    在 JavaScript 中,很多设计模式都是通过闭包和高阶函数实现的。相对于模式的实现过程,我们更关注的是模式可以帮助我们完成什么。


posted on 2017-01-20 12:19  Ruth92  阅读(274)  评论(0编辑  收藏  举报

导航