首页 技术 正文
技术 2022年11月11日
0 收藏 641 点赞 3,859 浏览 11729 个字

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

相关推荐
python开发_常用的python模块及安装方法
adodb:我们领导推荐的数据库连接组件bsddb3:BerkeleyDB的连接组件Cheetah-1.0:我比较喜欢这个版本的cheeta…
日期:2022-11-24 点赞:878 阅读:9,071
Educational Codeforces Round 11 C. Hard Process 二分
C. Hard Process题目连接:http://www.codeforces.com/contest/660/problem/CDes…
日期:2022-11-24 点赞:807 阅读:5,549
下载Ubuntn 17.04 内核源代码
zengkefu@server1:/usr/src$ uname -aLinux server1 4.10.0-19-generic #21…
日期:2022-11-24 点赞:569 阅读:6,397
可用Active Desktop Calendar V7.86 注册码序列号
可用Active Desktop Calendar V7.86 注册码序列号Name: www.greendown.cn Code: &nb…
日期:2022-11-24 点赞:733 阅读:6,174
Android调用系统相机、自定义相机、处理大图片
Android调用系统相机和自定义相机实例本博文主要是介绍了android上使用相机进行拍照并显示的两种方式,并且由于涉及到要把拍到的照片显…
日期:2022-11-24 点赞:512 阅读:7,809
Struts的使用
一、Struts2的获取  Struts的官方网站为:http://struts.apache.org/  下载完Struts2的jar包,…
日期:2022-11-24 点赞:671 阅读:4,889