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

系列文章

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指向该对象

1
2
3
4
5
6
7
8
9
var obj = {
name:'moe',
getName:function() {
console.log(this === obj) // true
console.log(this.name) // moe
}
}

obj.getName()

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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指向返回的这个对象。
1
2
3
4
5
6
var MyClass = function() {
this.name = 'moe'
}

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

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

1
2
3
4
5
6
7
8
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

1
2
3
4
5
6
7
8
9
10
11
12
13
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指向,从第二个参数往后,每个参数被依次传入函数。
1
2
3
4
5
6

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,可以实现类似于继承的效果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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。

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

闭包和高阶函数

闭包

变量作用域

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

1
2
3
4
5
6
7
8
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

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

1
2
3
4
5
6
7
8
9
10
11
12
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()

变量的生存周期

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

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

1
2
3
4
5
6
7
8
9
10
11
var func = function() {
var a = 1
return function() {
a++
console.log(a)
}
}
var f = func()
f() // 2
f() // 3
f() // 4

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

1
2
3
4
5
6
7
8
9
10
/*
以下打印结果是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保存到该函数的参数中,延长其生命周期。

1
2
3
4
5
6
7
8
for (var i = 0; i < 5; i++) { 
(function(j) {
setTimeout(function() {
console.log(j);
}, 1000);
})(i);
}
console.log(i);

闭包的更多作用

  1. 封装变量

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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. 延续局部变量寿命
1
2
3
4
5
6
7
8
var report = (fucntion() {
var imgs = []
return function(src) {
var img = new Image()
imgs.push(img)
img.src = src
}
})

闭包和面向对象设计

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// 闭包写法
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

用闭包实现命令模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
<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. 回调函数

    • 异步回调
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13

    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)
    })

    • 事件委托
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    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
2
3
4
5
6
7
8
// 从小到大
[1, 4, 3].sort((a, b) => {
return a - b
})
// 从大到小
[1, 4, 3].sort((a, b)=> {
return b - a
})

函数作为返回值输出

  1. 判断数据类型
1
2
3
4
5
6
7
8
9
10
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

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

1
2
3
4
5
6
var getSingle = function ( fn ) { 
var ret;
return function () {
return ret || ( ret = fn.apply( this, arguments ) );
};
};

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

1
2
3
4
5
6
var getScript = getSingle(function(){ 
return document.createElement( 'script' );
});
var script1 = getScript();
var script2 = getScript();
console.log( script1 === script2 ); // true

高阶函数实现AOP

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
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)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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最常见的应用场景之一。

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

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

1
2
3
4
5
6
7
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函数。

1
2
3
4
5
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时发生了什么。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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. 函数节流(不展开说了,八股文系列)

w字总结《JavaScript设计模式与开发实践》(基础篇)
https://moewang0321.github.io/2021/11/16/w字总结《JavaScript设计模式与开发实践》(基础篇)/
作者
Moe Wang
发布于
2021年11月16日
许可协议