从纯函数库👀js特性 上

270 阅读7分钟

配合源码地址:heihei12305

配合gitbook小书地址 my-underscore

写在前面,这本小书内容大量源于

  1. 讶羽博客系列
  2. JavaScript Es6 函数式编程入门教程
  3. 对角的lodash源码分析
  4. underscore源码分析

1. 将代码都挂载到_上,通过_.引用

目标:

  • 支持函数式风格调用: _.reverse('hello');
  • 支持面向对象风格调用: _('hello').reverse();
  • 支持链式调用 _.chain([1,2,3,4]).filter((num)=>!num%2).map((num)=>num*num).value();

下面我们一点一点的说吧。

首先为了达到可以用_调用的目的,我们需要先定义出_,也即:

var root = (typeof self == 'object' && self.self == self && self) ||
        (typeof global == 'object' && global.global == global && global)||
        this || {};
				
        //在浏览器中,除了window属性,我们也可以通过self属性直接访问到Window对象,同时self还可以支持Web Worker
        //Node环境中全局变量为 global
        //node vm(沙盒模型) 中不存在window,也不存在global变量,但我们却可以通过this访问到全局变量
        //在微信小程序中,window和global都是undefined,加上强制使用严格模式,this为undefined,就多了{}

var _ = function(obj){
        
        if(obj instanceof _){
            return obj;
        }
        
        if(!(this instanceof _)){ // 此处为了实现面向对象风格调用,可以暂时不管
            return new _(obj);
        }
        this._wrapped = obj;
    };

 //exports.nodeType 防止<div id="exports"></div>产生的window.exports全局变量。
 
    if(typeof exports != 'undefined' && !exports.nodeType){
        if((typeof module != 'undefined' && !module.nodeType && module.exports)){
				
        //在nodeJs中,exports是module.exports 的一个引用,当你使用了module.exports = function(){}
        //实际上覆盖了module.exports,但是exports并未发生改变,为了避免后面在修改exports而导致不能正确输出
        //写成这样,将两者保持统一。
				
            exports = module.exports = _;
        } 
        exports._ = _;
    }else{
       //?不太懂部分,将_挂到全局属性_上
       root._ = _;
    }

上面代码中,我们首先定义了全局属性,考虑node,浏览器,微信小程序等不同的环境,我们进行了一长串的属性选择。最终获得了我们需要的root属性。然后将_挂到了root上。这一块可以看看讶羽大大的博客写underscore的第一二章部分


有了_,我们就可以开始把我们写的函数都挂到_上了,这个简单,定义的时候这么写就好了 _.reverse = ()=>;

为了实现面向对象风格调用。


示例分析:_([1,2,3]):

var _ = function(obj){ 
    if(!(this instanceof _)){ // 此处为了实现面向对象风格调用,可以暂时不管
        return new _(obj);
    }
    this._wrapped = obj;
		//部分
};
  1. 执行this instanceof _ ,this指向 window,window instanceof _为 false,!操作符取反,所以执行 new _(obj).
  2. new _(obj)中,this指向实例对象,window instanceof _ 为 true,取反后,代码接着执行。
  3. 执行 this._wrapped = obj, 函数执行结束。
  4. 总结,_([1,2,3])返回一个对象,为{_wrapped:[1,2,3]},该对象原型指向_.prototype

也即我们下一步目标:将_上的函数也挂到_.prototype上。

先把_上的函数获取一下吧。

functions()

 //将obj中所有函数均push进names中
    _.functions = function(obj){
        var names = [];
        for(var key in obj){
            if(_.isFunction(obj[key])){
                names.push(key);
            }
        }
        return names.sort();
    }

each()


一个源于jQuery的通用遍历方法,可用于遍历对象和数组
回调函数拥有两个参数:第一个为对象的成员或数组的索引,第二个为对应变量或内容
而且可以退出循环

//数组遍历
$.each([0,1,2],function(i,n){
		console.log('item # '+ i + ": " + n)
})
//item #0:0
//item #1:1
//item #2:2

//对象遍历
$.each({name;"John",lang:"JS"},function(i,n){
		console.log('name: '+ i + ",value: " + n)
})
//item name,value:John
//name:lang,value:JS

//退出循环
$.each([0,1,2,3,4,5],function(i,n){
	if(i>2){
		return false;
	}
	console.log("item #"+i+": " + n );
});
//item #0:0
//item #1:1
//item #2:2

_.each = function(obj,callback){
	var length,i = 0;

	//判断类数组对象和数组
	if(_.isArrayLike(obj)){
		//为数组时
		length = obj.length;
		for(;i<length;i++){
		//绑定this到当前遍历元素上,但是call对性能有一丢丢影响
		if(callback.call(obj[i],obj[i],i) === false){
	    	//当回调函数返回false的时候,我们就中止循环
				break;
			}
		}
		}else{
			//为对象时
			for( i in obj){
				if(callback.call(obj[i],obj[i],i) === false){
					break;
				}
			}
		}
		return obj;
	}
}

有了上面两个函数,我们就可以来写我们的主角 mixin()

回顾一下

因为_([1,2,3])返回一个为{_wrapped:[1,2,3]}的原型指向_.prototype的对象
为了调用_函数对象上的方法,我们要把_上的方法复制到_.prototype

 _.mixin = function(obj){
    _.each(_.functions(obj),function(name){
        var func = _[name] = obj[name];
        //原型链的函数在这里定义!调用的时候就会跳到这里了。
        _.prototype[name] = function(){
           var args = [this._wrapped];
            
            push.apply(args,arguments);

            return chainResult(this,func.apply(_,args)); //此处为了链式调用可以暂时不管
        };
    });

    return _;
}

这里我们就可以用对象风格和函数风格来调用我们的函数库里的文件了,现在,我们来实现链式调用。


链式调用

回顾一下:
链式调用例子 : _.chain([1,2,3,4]).filter((num)=>!num%2).map((num)=>num*num).value();

就依照上面那个函数走好了

为了实现链式调用,我们首先需要调用_.chain()处理以下我们的参数

 _.chain = function(obj){
    var instance = _(obj);
    instance._chain = true;
    return instance;
}

源码很简单,我们把传入的obj处理了一下,结合上面的_函数,回到顶部,传入[1,2,3,4],返回值会是

{
_wrapped : [1,2,3,4];
_chain : true;
}

然后我们需要一个函数来判断我们刚添加的_chain属性,也即 chainResult()

//为了判断是否有_.chain(),即是否采用链式调用
    var chainResult = function (instance, obj){
        return instance._chain?_(obj).chain():obj;
    }

然后,在我们在mixin()中调用即可:chainResult(this,func.apply(_,args));

回顾一下:

//mixin()函数部分
_.prototype[name] = function(){
	var args = [this._wrapped];
	push.apply(args,arguments);
	return chainResult(this,func.apply(_,args));
};
        

我们在调用_.prototype上的函数时,会跳转到mixin()中也即我们定义原型链函数的地方,然后return的时候会调用chainResult函数,然后返回值就会被处理,也即被调用的函数都会返回一个带有_chain : true的对象。


但是我们最后的函数返回值依旧是一个对象,我们需要的是里面的_wrapped,这里就需要我们的_.value()了:

_.prototype.value = function(){
    return this._wrapped;
    }

小节结语,这里,我们完成了一个函数库最基础的布置,下面的章节我们开始向我们的函数库填充抽象出的可复用函数啦!


类数组之isArrayLike()

类数组定义:拥有一个length 属性和若干索引属性的对象

例如:

  • arguments
  • 有length属性的对象
a = {
	1:1,
	2:2,
	c:3,
	length:4
}

console.log(_.isArrayLike(a));//true
_.isArrayLike = function(collection){
    var length = collection.length;
    return typeof length == 'number' && length >= 0 && length <= MAX_ARRAY_INDEX;
}

其中,MAX_ARRAY_INDEX指的是最大安全数

var MAX_ARRAY_INDEX = Math.pow(2,53) - 1;

关于最大安全数,不懂的话可以看看这个JavaScript 浮点数陷阱及解法

函数判断之isFunction()

 _.isFunction = function(obj){
    return typeof obj == 'function' || false;
}

就是简单判断一个函数而已,不过可以扩展出一个知识点嘿嘿,

你是怎么看js中的&&,||

与其说是逻辑运算符,不如说是属性选择符。

  • &&执行方式(a&&b):
    • 如果a为true,返回b
    • 否则返回a
  • ||执行方式(a || b):
    • 如果a为true, 返回a
    • 否则返回b
'' || [1,2] //[1,2]
'' && [1,2] //""

[1] || [] //[1]
[1] && [] //[]

然后把js中为false的属性整理一下啦:
ES5规范9.2定义了抽象操作ToBoolean,列举了布尔强制类型转换所有可能出现的结果。

  • undefined
  • null
  • false
  • +0,-0 和 NaN
  • ""

从逻辑上说,假值列表以外的都应该是真值。

  • 除了'',其他的字符串都是真值。
let a  = 'false';
let b = '0';
let c = "''";

let d = Boolean(a && b && c );
d; // true
  • [],{},fucntion(){}都不在假值列表里,所以他们都是真值。
let a  = [];
let b = {};
let c = function(){};

let d = Boolean(a && b && c );
d; // true

真值列表是无限长的,我们只能根据假值列表作为参考,可以理解位假值列表以外的值都是真值。


  • 常用的 || 用法:设置默认值
function foo(a,b){
    a = a || 'hello';
    b = b || 'world';

    console.log(a + ' ' + b);
};

foo(1);// '1 world'


  • js代码压缩工具常用的‘守护运算符’:

function foo(){
    console.log(a);
};

let a = 1;
a && foo(); 
//当第一个操作数为真值时,&&才会选择第二个操作数作为返回值,即前面的表达式为后面的表达式'把关'

字符反转 reverse()

js的字符不可变,故而反转思路是 字符串->字符数组->反转->字符串

_.reverse = function(string){
        return string.split('').reverse().join('');
    }

这是一种取巧的做法,过程相当于

'1234'=>['1','2','3','4']=>['4','3','2','1']=>'4321'