zoukankan      html  css  js  c++  java
  • 《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 中,很多设计模式都是通过闭包和高阶函数实现的。相对于模式的实现过程,我们更关注的是模式可以帮助我们完成什么。


  • 相关阅读:
    CodeSmith 破解
    overflow 及其属性
    Override any CSS style (even inline styles) no matter where it is defined
    CSS驱动的下拉菜单
    对zindex的重新学习
    Eeic Meyer on CSS 之 文字排列成曲线
    Eeic Meyer on CSS 之 背景半透明效果
    background背景窍门
    CSS实现网页图片预加载
    meta 标签举例
  • 原文地址:https://www.cnblogs.com/Ruth92/p/6322491.html
Copyright © 2011-2022 走看看