JavsScript 设计模式(一)

JavsScript 设计模式(一)

读完本文可以学到

  1. this、call、apply、bind
  2. 闭包
  3. 什么是高阶函数
  4. 高阶函数的应用,包括AOP、currying、debounce等

[TOC]

之所以在学习设计模式之前,先理解这些内容,是因为设计模式里面很多函数作为参数,或者作为返回的值,还有很多call和apply的使用。

1. this、call、apply和bind

1.1 this

我们知道,this指向一个对象,具体的指向由运行时基于函数的执行环境动态来绑定的,需要注意的地方时,this不一定指向函数被声明时的环境。总结来说就是this指向最后调用它的那个对象。

看一个例子:

var outername = 'window';
var obj = {
    fn: function(){
        console.log(this.name);
    }
}
console.log(obj.fn())

输出的结果是undefined,因为最后调用fn的对象是obj,而对象obj中没有name的定义,所有就是undefined。这说明this永远指向最后调用它对象,也不会继续向上一个对象寻找this.name。

改变this的指向,除了不常用的with和eval之外,通常有5种方式

  • 作为对象的方法调用
    当作为函数的方法调用时,this指向该对象
var outername = 'window';
var obj = {
    fn: function(){
        console.log(this.name);
        console.log(this === obj);
    }
}

obj.fn()
  • 作为普通函数调用
    此时this指向window对象
var outername = 'window';
var obj = {
    fn: function(){
        console.log(this.name);
        console.log(this === obj);
    }
}

var objFn = obj.fn;
objFn();
  • 作为构造函数调用
    通常情况下,构造器里面的this就是指向返回的这个对象
var Myconstructor = function (){
    this.name ='inner'
};

var myInstance = new Myconstructor();
myInstance.name;

这里需要注意的是,如果构造函数中没有显示的返回一对象,那么this指向的就是构造函数,否则指向返回的那个对象。

var Myconstructor = function (){
    this.name ='inner';
    return {
        name: 'inreturn',
    };
};

var myInstance = new Myconstructor();
myInstance.name;
  • ES6 的箭头函数
    箭头函数的this始终指向函数定义时的this,而非执行时。

  • 通过Function.prototype.call或者Function.prototype.apply来调用
    call和apply可以动态的传入函数的this

var obj = {
 name: 'obj';
 getName: function(){
   return this.name;
 }
}

var obj1 = {
  name: 'obj1'
}

obj.getName()
obj.getName.call(obj1);

1.2 call

语法:

fun.call(thisArg[, arg1[, arg2[, ...]]])

thisArg代表fun运行时指定的this的值

call() 方法调用一个函数, 其具有一个指定的this值和分别地提供的参数(参数的列表)

我们先看个例子:

var foo = {
    value: 1
};

function bar() {
    console.log(this.value);
}

bar.call(foo); // 1

如何来简单的实现这个call方法呢?
我们把上面的代码修改成:

var foo = {
    value: 1,
    bar: function() {
        console.log(this.value)
    }
};

foo.bar(); // 1

可以看到,this指向了foo,但是缺点是在foo上增加了一个方法,不过没关系,我们用完删了不就可以了,所以我们来看看步骤:

  • 把函数设置为对象的属性
  • 执行该函数
  • 删除该函数
Function.prototype.call2 = function(context) {
    // 首先要获取调用call的函数,用this可以获取
    context.fn = this;
    context.fn();
    delete context.fn;
}

看一个如何模拟call方法

Function.prototype.call = function(context) {
  var context = context || window;
  context.fn = this;
  var args = Array.prototype.slice.call(arguments,1);
  var result = context.fn(...args);
  delete context.fn
  return result;
} 

如何去模拟实现call可以参考
https://github.com/mqyqingfeng/Blog/issues/11

1.3 apply

语法:

fun.apply(thisArg, [argsArray])

apply() 方法调用一个函数, 其具有一个指定的this值,以及作为一个数组(或类似数组的对象)提供的参数

apply和call的区别在于传入参数不一样,但是还有一个比较大的区别,call的运行效率要比apply的高。
前段时间看了underscore的源码,其中有这么一部分

var optimizeCb = function(func, context, argCount) {
    if (context === void 0) return func;
    switch (argCount == null ? 3 : argCount) {
      case 1: return function(value) {
        return func.call(context, value);
      };
      case 2: return function(value, other) {
        return func.call(context, value, other);
      };
      case 3: return function(value, index, collection) {
        return func.call(context, value, index, collection);
      };
      case 4: return function(accumulator, value, index, collection) {
        return func.call(context, accumulator, value, index, collection);
      };
    }
    return function() {
      return func.apply(context, arguments);
    };
  };

其他中间那些func.call是可以不写的,直接最后return func.apply不就可以了么。

apply的模拟方法和call类似就不说啦

1.4 bind

语法:

fun.bind(thisArg[, arg1[, arg2[, ...]]])

bind()方法创建一个新的函数, 当被调用时,将其this关键字设置为提供的值,在调用新函数时,在任何提供之前提供一个给定的参数序列。也就是说bind方法是创建一个方法,我们必须手动的调用

作用:

  • 创建绑定函数,最简单的用法是创建一个函数,使这个函数不论怎么调用都有同样的 this 值
this.x = 9; 
var module = {
  x: 81,
  getX: function() { return this.x; }
};

module.getX(); // 返回 81

var retrieveX = module.getX;
retrieveX(); // 返回 9, 在这种情况下,"this"指向全局作用域

// 创建一个新函数,将"this"绑定到module对象
// 新手可能会被全局的x变量和module里的属性x所迷惑
var boundGetX = retrieveX.bind(module);
boundGetX(); // 返回 81
  • 偏函数, 另一个最简单的用法是使一个函数拥有预设的初始参数
function list() {
  return Array.prototype.slice.call(arguments);
}

var list1 = list(1, 2, 3); // [1, 2, 3]

// Create a function with a preset leading argument
var leadingThirtysevenList = list.bind(undefined, 37);

var list2 = leadingThirtysevenList(); // [37]
var list3 = leadingThirtysevenList(1, 2, 3); // [37, 1, 2, 3]
  • 配合setTimeout
function LateBloomer() {
  this.petalCount = Math.ceil(Math.random() * 12) + 1;
}

// Declare bloom after a delay of 1 second
LateBloomer.prototype.bloom = function() {
  window.setTimeout(this.declare.bind(this), 1000);
};

LateBloomer.prototype.declare = function() {
  console.log('I am a beautiful flower with ' +
    this.petalCount + ' petals!');
};

var flower = new LateBloomer();
flower.bloom();  // 一秒钟后, 调用'declare'方法

我们都知道bind方法是有兼容性问题的,那么我们怎么实现一个bind方法呢?
我们看看最简单的版本

Function.prototype.bind = function(context) {
    var self = this;
    return function() {
        self.apply(context);
    };
};

进阶版本:

Function.prototype.bind = function(context) {
    var self = this;
    var args = Array.prototype.slice.call(arguments, 1);
    return function() {
        var bindArgs = Array.prototype.slice.call(arguments);
        self.apply(context, arg.contact(bindArgs));
    };
}

可以传参数,可以返回函数了,但是还有一个问题没解决,因为bind方法还有一个特点:
一个绑定函数也能使用new操作符创建对象:这种行为就像把原函数当成构造器。提供的 this 值被忽略,同时调用时的参数被提供给模拟函数。

MDN上的版本

if (!Function.prototype.bind) {
  Function.prototype.bind = function(oThis) {
    if (typeof this !== 'function') {
      // closest thing possible to the ECMAScript 5
      // internal IsCallable function
      throw new TypeError('Function.prototype.bind - what is trying to be bound is not callable');
    }

    var aArgs   = Array.prototype.slice.call(arguments, 1),
        fToBind = this,
        fNOP    = function() {},
        fBound  = function() {
          return fToBind.apply(this instanceof fNOP
                 ? this
                 : oThis,
                 // 获取调用时(fBound)的传参.bind 返回的函数入参往往是这么传递的
                 aArgs.concat(Array.prototype.slice.call(arguments)));
        };

    // 维护原型关系
    if (this.prototype) {
      // Function.prototype doesn't have a prototype property
      fNOP.prototype = this.prototype; 
    }
    fBound.prototype = new fNOP();

    return fBound;
  };
}

2. 闭包

2.1 概念

MDN 对闭包的定义为:

闭包是指那些能够访问自由变量的函数

那么这里我们需要理解什么是自由变量

自由变量是指在函数中使用的,但既不是函数参数也不是函数的局部变量的变量

举个例子:

var a = 1;
function foo() {
    console.log(a);
}

foo();

foo 函数可以访问a,并且a不是函数参数也不是函数局部变量,所有foo函数和a就形成了闭包。

理论上来说所有的函数都是闭包,但是在实践角度来说闭包的定义是:

  • 首先是一个函数
  • 函数的上下文即使已经销毁,它依然存在
  • 函数的代码中应用了自由变量

我们先看一个简单的例子:

var func = function () {
    var a = 1;
    return function() {
        a++;
        alert(a);
    }
}

var f = func();

f();
f();
f();

我们可以看到,当退出函数之后,局部变量a并没有消失。只是因为在执行var f = func()时,f返回了一个匿名函数的引用,这个函数可以访问到a变量,所以a没有被销毁

2.2 应用

  • 封装变量
    闭包可以把一些不需要暴露在全局变量中的变量封装为私有变量
var mult = function() {
    var a = 1;
    for(var i = 0; i < arguments.length; i++) {
        a = a * arguments[i];
    }
    return a;
}

这个函数本身没啥问题,那么如果我们需要把算出来的结果缓存一波,怎么办呢?

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, l = arguments.length; i < l; i++) {
        a = a * arguments[i];
    }
    return cache[args] = a;
}

这样似乎可以解决问题,但是caches暴露在了外部,我们可能改变它的值。


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, l = arguments.length; i < l; i++) {
            a = a * arguments[i];
        }
        return cache[args] = a;
    }
})()

我们看到这个函数和上面没啥大的区别,就是用一个立即执行函数把之前的mult函数包裹了起来,然后返回。

  • 延续局部变量的寿命

我们看一个例子:

var report = function(src) {
    var img = new Image();
    img.src = src;
}

report('http://xxxx.img.jpg')

这个代码看起来好像没啥问题,但是在IE7之类的低版本浏览器会有问题,report函数并不是每次都可以发起http请求,原因就是img是report的局部变量,当report函数调用结束之后,img局部变量就销毁了,这个时候有可能还么发起http请求,所有请求就丢失掉啦。

var report = (function() {
    var imgs = [];
    return function(src) {
        var img = new Image();
        imgs.push(img);
        img.src = src;
    }
})()

2.3 闭包与设计模式

在设计模式中,有很多的地方用到了闭包,例如命令模式等等。后续再详细的介绍

3. 高阶函数概念

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

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

3.1 函数作为参数传递

  • 作为回调函数
    如要我们需要对业务做一个流程的控制,可以利用回调函数来实现,例如做完A,然后才去做B
var doA = function (callback) {
    console.log('do a')
    callback();
}

var doB = function(){
    console.log('do b');
}

doA(doB);
  • Array.prototype.sort
    接受一个函数作为参数

3.2 函数作为返回值

其实签名的例子就有函数作为返回值的例子,我们接下来看两个例子

  • 判断数据类型
    我们之前其实知道了如何去判断一个数据是否为数组或者字符串
var isArray = function(obj) {
    return Object.prototype.toString.call(obj) == '[object Array]'
};
var isString = function(obj) {
    return Object.prototype.toString.call(obj) == '[object String]'
};

其实这些代码都是重复的,我们可以返回一个函数来减少代码

var isType = function(type) {
    return function(obj) {
        return Object.prototype.toString.call(obj) == '[object ' + type + ' ]';
    };
}

var isString = isType('String');
var isArray = isType('Array');
  • getSingle
    我们再来看一个单例模式的例子:
var getSingle = function ( fn ) {
     var ret; 
     return function () { 
        return ret || ( ret = fn.apply( this, arguments ) ); 
     }; 
};

我们可以看到这里既把函数作为了输出,又把函数作为了返回值,我们看单例模式的应用:

var getNode = getSingle(function(){
    return document.createElement('script');
});

var node1 = getNode();
var node2 = getNode();

node1 === node2

4. 高阶函数的应用

4.1 高阶函数与AOP

AOP 也叫做面向切面编程,作用就是把一些和核心业务逻辑无关的代码抽离出来,比如说:日志处理、安全控制、异常处理等等。在Java中可以利用反射和动态代理来实现AOP,而对于JavaScript这种动态语言来说,它天生就有这种能力,利用Function.prototype就可以很容易做到AOP编程

Function.prototype.before = function(beforefn) {
    var _self = this;
    // 这里保持了原函数的引用
    return function(){
        // 返回包含了原函数和新函数的代理函数
        beforefn.apply(this, arguments);
        // 执行函数,并且保证this不被劫持,新函数接受的参数也会被传入原来的函数,新函数在原函数之前执行
        return _self.apply(this, arguments);
        // 执行原函数并返回原函数的执行结果
        // 并且保证this不被劫持
    }
}

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.2 高阶函数与柯里化

柯里化定义

In mathematics and computer science, currying is the technique of translating the evaluation of a function that takes multiple arguments (or a tuple of arguments) into evaluating a sequence of functions, each with a single argument.

柯里化作用

  • 延迟计算
  • 参数复用
  • 动态生产函数的作用

看个例子吧

function add(a, b) {
    return a + b;
}
add(1, 2) // 3

// 假设有一个 curry 函数可以做到柯里化
var addCurry = curry(add);
addCurry(1)(2) // 3

用闭包把参数保存起来,当参数的数量足够执行函数了,就开始执行函数,有没有毛病

我们看一个版本的实现

function curry(fn, args) {
    var length = fn.length;

    args = args || [];

    return function() {

        var _args = args.slice(0),

            arg, i;

        for (i = 0; i < arguments.length; i++) {

            arg = arguments[i];

            _args.push(arg);

        }
        if (_args.length < length) {
            return curry.call(this, fn, _args);
        }
        else {
            return fn.apply(this, _args);
        }
    }
}


var fn = curry(function(a, b, c) {
    console.log([a, b, c]);
});

fn("a", "b", "c") // ["a", "b", "c"]
fn("a", "b")("c") // ["a", "b", "c"]
fn("a")("b")("c") // ["a", "b", "c"]
fn("a")("b", "c") // ["a", "b", "c"]

再看一个高颜值的写法

var curry = fn =>
    judge = (...args) =>
        args.length === fn.length
            ? fn(...args)
            : (arg) => judge(...args, args

4.3 高阶函数与函数防抖

其实函数防抖以前分享过一次,但是只是分享了场景和underscore的api,这里我们仔细看一下如何去实现一个函数防抖

防抖的原理就是,随便怎么触发事件,但是事件只会在n秒之后才执行,但是如果在n秒内你又触发了,那就重新计时,总之就是我一定要等你触发完事件了,并且n秒内不再触发,我才执行

先看第一版

function debounce(func, wait) {
    var timeout;
    return function() {
        clearTimeout(timeout);
        timeout = setTimeout(func, wait);
    }
}

container.onmousemove = debounce(doSomething, 1000);

如果不使用debounce函数, doSomething中的this指向的是container函数,使用了之后this指向了window对象,所以上面的版本是有问题的

function debounce(func, wait) {
    var timeout;
    return function(){
        clearTimeout(timeout);
        var context = this;
        timeout = setTimeout(function(){
        func.apply(context);
        }, wait);  
    };
}

但是这个函数还是有问题,因为dosomething(e)中的e在经过debounce后打印出来的是undefined。其实只需要把参数绑定到新的函数就可以啦

function debounce(func, wait) {
    var timeout;
    return function(){
        clearTimeout(timeout);
        var context = this;
        var args = arguments;
        timeout = setTimeout(function(){
        func.apply(context, args);
        }, wait);  
    };
}

我们做了两件事

  • 改变了this的指向
  • 绑定了参数event

我们可以考虑一个新的需求,我们希望事件立即被触发,然后等停止触发n秒后,才重新触发函数

function debounce(func, wait, immediate) {

    var timeout, result;

    return function () {
        var context = this;
        var args = arguments;

        if (timeout) clearTimeout(timeout);
        if (immediate) {
            // 如果已经执行过,不再执行
            var callNow = !timeout;
            timeout = setTimeout(function(){
                timeout = null;
            }, wait)
            if (callNow) func.apply(context, args)
        }
        else {
            timeout = setTimeout(function(){
                func.apply(context, args)
            }, wait);
        }
    }
}

接下来我们看看underscore的实现

  _.debounce = function(func, wait, immediate) {
    var timeout, args, context, timestamp, result;

    var later = function() {
      var last = _.now() - timestamp;

      if (last < wait && last >= 0) {
        timeout = setTimeout(later, wait - last);
      } else {
        timeout = null;
        if (!immediate) {
          result = func.apply(context, args);
          if (!timeout) context = args = null;
        }
      }
    };

    return function() {
      context = this;
      args = arguments;
      timestamp = _.now();
      var callNow = immediate && !timeout;
      if (!timeout) timeout = setTimeout(later, wait);
      if (callNow) {
        result = func.apply(context, args);
        context = args = null;
      }

      return result;
    };
  };

参考资料:

书籍:《JavaScript设计模式与实践》
博客链接:https://github.com/mqyqingfeng/Blog/issues/22

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,098评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,213评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,960评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,519评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,512评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,533评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,914评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,574评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,804评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,563评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,644评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,350评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,933评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,908评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,146评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,847评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,361评论 2 342

推荐阅读更多精彩内容