JavsScript 设计模式(一)
读完本文可以学到
- this、call、apply、bind
- 闭包
- 什么是高阶函数
- 高阶函数的应用,包括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