w字总结《JavaScript设计模式与开发实践》(基础篇)

2,837 阅读7分钟

「这是我参与11月更文挑战的第1天,活动详情查看:2021最后一次更文挑战

系列文章

w字总结《JavaScript设计模式与开发实践》(设计模式)(上)

w字总结《JavaScript设计模式与开发实践》(设计模式)(下)

w字总结《JavaScript设计模式与开发实践》(设计原则和编程技巧)

前言

为什么我要开始写这类文章了,一切都得从EDG夺冠开始说起,在打完第三局后我发了一条朋友圈……如下图。

我也是没想到EDG韧性这么强,汉子哥这么热爱中国。 不管怎么说,恭喜EDG,自己立的flag,哭着也要把它实现。(其实蛮不错的,充实自己。) 现在每天和周末空闲时间就读一些,做做笔记,周末整合一下形成文章,往掘金这么一发。

this,call和apply

this

在JavaScript中,this指向的对象是在运行时基于函数的执行环境动态绑定的,而非声明函数时的环境

this指向

除去witheval这两种会“破坏”我们对作用域理解的情况,this的指向大致分为以下几种。

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

作为对象的方法调用时,this指向该对象

var obj = {
    name:'moe',
    getName:function() {
        console.log(this === obj) // true
        console.log(this.name) // moe
    }
}

obj.getName()

当函数不作为对象属性而作为普通函数调用时,this指向全局对象,在浏览器的JavaScript中,全局对象为window

window.name = 'moe'

var getName = function() {
    return this.name
}

var person = {
    name:'kid',
    getName:function() {
        return this.name
    }
}

var globalGetName = person.getName
console.log( getName() ) // moe
console.log( globalGetName() ) // moe

作为构造器调用

  1. 用new运算符调用时,该函数返回一个对象,通常情况下,构造器内this指向返回的这个对象。
var MyClass = function() {
    this.name = 'moe'
}

var obj = new MyClass()
console.log(obj.name) // moe

但如果构造器显式返回了一个object类型对象,则返回的为该对象,而不是上述的this。

var MyClass = function() {
    this.name = 'moe'
    return {
        name: 'kid'
    }
}
var obj = new MyClass()
console.log(obj.name) // kid

构造器不显式返回数据或返回非对象类型数据,则无上述问题。

Function.prototype.call或Function.prototype.apply调用可以动态改变传入函数的this

var obj1 = {
    name:'a',
    getName:function() {
        return this.name
    }
}

var obj2 = {
    name:'b'
}

console.log(obj1.getName()) // a
console.log(obj1.getName.call(obj2)) // b

call和apply

call和apply都是用来修改this指向,并执行函数,唯一的区别是入参形式不同

  • apply接受两个参数,第一个参数指定了函数体内this对象的指向,第二个参数为集合(数组或类数组),apply方法把这个集合中的元素作为参数传递给被调用的函数。
  • call本质上为apply的语法糖,它传入参数数量不定,第一个参数同apply一样,也是代表函数体内this指向,从第二个参数往后,每个参数被依次传入函数。

var foo = function(a, b, c) {
    console.log([a, b, c]) // [1, 2, 3]
}
foo.apply(null, [1, 2, 3])
foo.call(null, 1, 2, 3)

上述代码中,我们传入的第一个参数为null,则函数体内的this会指向默认的宿主对象。

call和apply的用途

  1. 改变this指向
  2. 借用其他对象方法

利用apply或call,可以实现类似于继承的效果。

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('newBee')
console.log( b.getName() ) // newBee
 

函数的arguments是一个类数组对象,因为其不是真正的数组,所以无法像数组一样进行排序或向集合中添加删除元素之类的操作。这种情况我们可以借用Array.prototype对象上的方法,比如push。

(function() {
    Array.prototype.push.call(arguments, 3);
    console.log(arguments); // [1, 2, 3]
})(1, 2)

闭包和高阶函数

闭包

变量作用域

在函数中声明变量,如果该变量前没有关键字var,该变量就会成为全局变量,而在函数中用var关键字声明的变量为该函数的局部变量,只有在该函数内才能访问到该变量。

var foo = function() {
    a = 1
    var b = 2
    console.log('in foo:' , b) // in foo: 2
}
foo()
console.log( a ) // 1
console.log( b ) // Uncaught ReferenceError: b is not defined

函数可以用来创造函数作用域,此时函数像一层半透明玻璃,函数内可以看到外面的变量,而函数外无法看到函数内部的变量。这是因为在函数中搜索一个变量时,若函数内没有声明这个变量,搜索则会随着代码执行环境创建的作用域链向外逐层搜索,直到全局对象。

var a = 1
var bar = function() {
    var b = 2
    var foo = function() {
        var c = 3
        console.log( b ) // 2
        console.log( a ) // 1
    }
    foo()
    console.log( c ) // Uncaught ReferenceError: c is not defined
}
bar()

变量的生存周期

  • 全局变量的生存周期为永久,除非主动销毁。
  • 函数内的局部变量会随着函数调用的结束而被销毁。

而闭包的存在可以让我们延续函数内局部变量的生命周期

var func = function() {
    var a = 1
    return function() {
        a++
        console.log(a)
    }
}
var f = func()
f() // 2
f() // 3
f() // 4

类似的,我们一定遇到过这样的题目

/*
    以下打印结果是5个5,为什么,如何打印出0到4?
*/
for (var i = 0; i < 5; i++) {
    setTimeout(function() { 
        console.log( i ); 
    }, 1000); 
} 
console.log(new Date, i);

在这个函数里面的i其实引用的是最后一次i的值,为什么不是0,1,2,3,4…呢?因为在你for循环的时候,你并没有执行这个函数,你这个函数是过一秒才执行的,当执行这个函数的时候,它发现它自己没有这个变量i,于是向它的作用域链中查找这个变量i,因为这个时候已经for循环完了,所以储存在作用域链里面的i的值就是5,最后就打印出来5了。

利用闭包解决,通过自执行函数,将变量i保存到该函数的参数中,延长其生命周期。

for (var i = 0; i < 5; i++) { 
    (function(j) {  
    	setTimeout(function() { 
    	    console.log(j);
        }, 1000); 
    })(i); 
}
console.log(i);

闭包的更多作用

  1. 封装变量

如果一大块代码中中存在可独立的小代码块,我们通常将其封装在独立的小函数中,独立出来的小函数有助于复用,如果它们不需要在程序的其他地方使用,最好用闭包将它们封闭。

var mult = (function() {
    var cache = {}
    var calculate = function() {
        var a = 1
        for(var i = 0, l = arguments.length; i < l; 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)
    }
})

  1. 延续局部变量寿命
var report = (fucntion() {
    var imgs = []
    return function(src) {
        var img = new Image()
        imgs.push(img)
        img.src = src
    }
})

闭包和面向对象设计

// 闭包写法
var extent = function() {
    var value = 0
    return {
        call:function() {
            value++
            console.log(value)
        }
    }
}

var extent = extent();
extent.call(); // 输出:1 
extent.call(); // 输出:2 
extent.call(); // 输出:3

// 面向对象
var extent2 = {
    value:0,
    call:function() {
        this.value++
        console.log(this.value)
    }
}

extent2.call(); // 输出:1 
extent2.call(); // 输出:2 
extent2.call(); // 输出:3

用闭包实现命令模式

<html> 
    <body>
        <button id="undo">点击我执行命令</button>
        <button id="execute">点击我执行命令</button>
        <script> 
            var Tv = { 
                 open: function(){ 
                    console.log( '打开电视机' ); 
                 }, 
                 close: function(){ 
                    console.log( '关上电视机' ); 
                 } 
            }; 
            var OpenTvCommand = function( receiver ){ 
                this.receiver = receiver; 
            }; 
            OpenTvCommand.prototype.execute = function(){ 
                this.receiver.open(); // 执行命令,打开电视机
            }; 
            OpenTvCommand.prototype.undo = function(){ 
                this.receiver.close(); // 撤销命令,关闭电视机
            }; 
            var setCommand = function( command ){ 
                 document.getElementById( 'execute' ).onclick = function(){ 
                    command.execute(); // 输出:打开电视机
                 } 
                 document.getElementById( 'undo' ).onclick = function(){ 
                    command.undo(); // 输出:关闭电视机
                 } 
            }; 
            setCommand( new OpenTvCommand( Tv ) ); 
        </script> 
    </body> 
</html>

高阶函数

即函数柯里化,指函数作为参数或函数作为返回值输出的函数。

函数作为参数传递

  1. 回调函数

    • 异步回调
    
    var getUserInfo = function(userId, cb) {
        $.ajax('http://xxx.com/getUserInfo?' + userId , function(data) {
            if(typeof cb === 'function') {
                cb(data)
            }
        })
    }
    
    getUserInfo(10086, (data)=> {
        console.log(data)
    })
    
    
    • 事件委托
    var appendDiv = function( callback ){ 
        for ( var i = 0; i < 100; i++ ){ 
            var div = document.createElement( 'div' ); 
            div.innerHTML = i; 
            document.body.appendChild( div ); 
            if ( typeof callback === 'function' ){ 
            callback( div ); 
            } 
        } 
    }; 
    appendDiv(function( node ){ 
        node.style.display = 'none'; 
    });
    
  2. Array.prototype.sort

Array.prototype.sort接受一个函数当作参数,函数内封装了数组元素的排序规则,我们只需要关注用什么规则排序,这是可变的,而对数组排序则是不变的。把可变的部分封装在函数参数里,动态传入Array.prototype.sort,使之更加灵活。

// 从小到大
[1, 4, 3].sort((a, b) => {
    return a - b
})
// 从大到小
[1, 4, 3].sort((a, b)=> {
    return b - a
})

函数作为返回值输出

  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 ] ) ); // 输出:true
  1. getSingle

单例模式,后续设计模式章节会有详细介绍

var getSingle = function ( fn ) { 
    var ret; 
    return function () { 
        return ret || ( ret = fn.apply( this, arguments ) ); 
    }; 
};

这里getSingle是一个高阶函数,将函数作为参数传递,又让函数执行后返回另一个函数,看看效果。

var getScript = getSingle(function(){ 
    return document.createElement( 'script' ); 
}); 
var script1 = getScript(); 
var script2 = getScript(); 
console.log( script1 === script2 ); // true

高阶函数实现AOP

AOP即面向切面编程,主要作用是将一些与核心业务逻辑无关的代码抽离,通常包括日志统计、安全控制、异常处理等。把这些功能抽离出来后,再通过“动态织入”的方式掺入业务逻辑模块,这样做的好处是可以保持业务逻辑的纯净与高内聚,且可以方便的复用抽离的功能模块。

Function.prototype.before = function(beforeFn) {
    var _self = this // 保存原函数的引用
    return function() { // 返回包含原函数和新函数的代理函数
        beforeFn.apply(this , arguments) // 执行新函数,修正this
        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 foo = function() {
    console.log('函数执行')
}

foo = foo.before(function() {
    console.log('before')
}).after(function() {
    console.log('after')
})

foo()

结果:

image.png

高阶函数的其他应用

  1. 函数柯里化(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, l = arguments.length; i < l; i++ ){ 
            money += arguments[ i ]; 
        } 
        return money; 
    } 
})(); 
var cost = currying( cost ); // 转化成 currying 函数
cost( 100 ); // 未真正求值
cost( 200 ); // 未真正求值 
cost( 300 ); // 未真正求值
alert ( cost() ); // 求值并输出:600
  1. uncurrying

简单来说,uncurrying函数是实现从别的对象中赋值方法,比如我们常常让类数组对象去借用Array.prototype上的方法,这是call和apply最常见的应用场景之一。

(function(){ 
    Array.prototype.push.call( arguments, 4 ); // arguments 借用 Array.prototype.push 方法
    console.log( arguments ); // 输出:[1, 2, 3, 4] 
})( 1, 2, 3 );

uncurrying用来解决将泛化this的过程提取出来的问题。下面是uncurrying的实现方式之一。

Function.prototype.uncurrying = function() {
    var self = this
    return function() {
        var obj = Array.prototype.shift.call(arguments);
        return self.apply(obj , arguments)
    }
}

先来看看它的作用是什么,在类数组对象arguments借用Array.prototype的方法之前,先把Array.prototype.push.call转换为一个通用的push函数。

var push = Array.prototype.push.uncurrying(); 
(function(){ 
    push( arguments, 4 ); 
    console.log( arguments ); // 输出:[1, 2, 3, 4] 
})( 1, 2, 3 );

通过uncurrying,将Array.prototype.push.call变成了一个通用函数,这样push的作用与Array.prototype.push相同,不仅仅局限于智能操作数组,而使用者对方法也更简洁和意图明了。现在通过push来看看调用uncurrying时发生了什么。

Function.prototype.uncurrying = function () { 
    var self = this; // self 此时是 Array.prototype.push 
    return function() { 
        var obj = Array.prototype.shift.call( arguments ); 
        // obj 是{ 
        // "length": 1, 
        // "0": 1 
        // } 
        // arguments 对象的第一个元素被截去,剩下[2] 
        return self.apply( obj, arguments ); 
        // 相当于 Array.prototype.push.apply( obj, 2 ) 
    }; 
}; 
var push = Array.prototype.push.uncurrying(); 
var obj = { 
    "length": 1, 
    "0": 1 
}; 
push( obj, 2 ); 
console.log( obj ); // 输出:{0: 1, 1: 2, length: 2}
  1. 函数节流(不展开说了,八股文系列)