掌握了JavaScript中这17个工具方法,做起事来事半功倍大大提升你的开发效率

178 阅读9分钟

数据类型检测

//建立数据类型检测映射表
var class2type = {};
var mapType = ["Boolean", "Number", "String", "Function", "Array", "Date", "RegExp", "Object", "Error", "Symbol"];
mapType.forEach(function(name){
	class2type["[object "+name+"]"] = name.toLowerCase();
});

//封装万能的数据类型检测方法
function toType(obj){
	//如果obj是null或undefined(undefined == null)则返回字符串null或字符串undefined
	if(obj == null) {
		return obj + "";
	};
	//基于字面量方式创建的基本数据类型,直接基于typeof检测即可(性能稍高一些)
	//剩余的则基于Object.prototype.toString.call的方式来检测,把获取的值拿到映射表中进行匹配,
	//得到的值是字符串对应的数据类型
	return typeof obj === "object" || typeof obj === "function" ?
		class2type[Object.prototype.toString.call(obj)] || "object" : typeof obj;
}

函数检测

//检测是否为函数
var isFunction = function isFunction(obj){
    //元素节点[DOM对象]具备nodeType(元素、文本、注释、document 对应1、3、8、9)
    //typeof obj.nodeType !== "number":防止在部分浏览器中,检测<object>元素对象结果也是"function"
    return typeof obj === "function" && typeof obj.nodeType !== "number";
}

window对象检测

//检测是否是window对象
var isWindow = function isWindow(obj){
	//window对象的特点:window.window === window
	return obj != null && obj === obj.window;
}

数组或类数组检测

//检测是否为数组或类数组
var isArrayLike = function isArrayLike(obj){
    //!!obj将obj转换为布尔类型
    //如果!!obj为true并且"length" in obj 为true则获取obj的length值
    //所以length存储的是对象的length属性或false
    //type存储的是检测的数据类型
    var length = !!obj && "length" in obj && obj.length,
            type = toType(obj);
    //window.length = 0 windows有length属性
    //Function.prototype.length = 0 Function的原型也有length属性
    //所以这里是排除window和Function,有length属性不一定就是数组
    if(isFunction(obj) || isWinow(obj)){
        return false;
    }
    //type === "array"说明是数组
    //length === 空的类数组
    //最后一个条件用于判断是非空数组:有length属性并且length是一个数字类型,length的值大于0,并且最大索引在数组中
    //length - 1就是最大索引
    return type === "array" || length === 0 || 
            typeof length === "number" && length > 0 && (length - 1) in obj;
            //逻辑与的优先级大于逻辑或
}

纯对象检测

//检测是否是纯对象,例如:{}
var isPlainObject = function isPlainObject(object){
    var proto, Ctor;
    //不存在,或基于toString检测的结果都不是[object Object]则一定不是对象
    if(!obj || Object.prototype.toString.call(obj) !== "[object Object]"){
         return false;
    }
    //获取当前值的原型链(直属类的原型链)
    proto = Object.getPrototypeOf(obj);
    //通过Object.create(null)创建的对象是没有__proto__ 的,所以肯定是纯对象
    if(!proto){
        return true;
    }
    //Ctor 存储原型对象上的constructor属性,如果没有这个属性就是false
    Ctor = Object.prototype.hasOwnProperty.call(proto, "constructor") && proto.constructor;
    //如果构造函数是一个函数,
    //条件成立说明原型上的构造函数是Object: obj就是Object的一个实例,并且obj.__proto__ === Object.prototype
    return typeof Ctor === "function" && Function.prototype.toString.call(Ctor) === Function.prototype.toString.call(Object);
    //将Object和Ctor转换为字符串,如果二者相等则条件成立
}

空对象检测

//检测是否为空对象
var isEmptyObject = function isEmptyObject(obj){
    //jQuery的原生写法,但是有缺点:基于for in循环有很多问题,无法获取Symbol类型的属性,会把自己定义在原型上的属性也获取到等等
    /*
    var name;
    for(name in obj){
        return false;
    }
    return true;
    */
    //排除null或undefined
    if(obj == null) return false;
    if(typeof !== "object") return false;
    //是一个对象,纯对象或特殊对象都可以
    var keys = Object.keys(obj);
    //如果兼容再去拼接
    if(Object.prototype.hasOwnProperty.call(Object,"getOwnPropertySymbols")){
        keys.concat(Object.getOwnPropertySymbols(obj));
    }
    return keys.length === 0;
}

数字检测

//检测是否为数字
var isNumeric = function isNumeric(obj){
    var type = toType(obj);//基于上面封装万能类型检测方法
    //纯数字或是数字的字符串形式("10") 并且不是NaN
    //obj - parseFloat(obj)只要有任何一个值不是数字,结果都是NaN, 可以直接用+obj代替
    return (type === "number" || type === "string") && !isNaN(obj - parseFloat(obj));
}

数组或对象通用循环

var each = function each(obj, callback){
    var length, i = 0;
    //数组或类数组处理
    if(isArrayLike(obj)){
        length = obj.length;
        for(; i < lenght; i++){
            if(callback.call(obj[i], i, obj[i]) === false){
                break;
            }
        }
    }else{//对象处理
        var keys = Object.keys(obj);//获取所有私有非Symbol类型的key
        //如果支持Symbol类型,则把Symbol类型的属性添加到keys中
        typeof Symbol !== "undefined" ? keys = keys.concat(Object.getOwnPropertySymbols(obj)) : null;
        for(; i < keys.length; i++){
            var key = keys[i];
            if(callback.call(obj[key], key, obj[key]) === false){
                break;
            }
        }
    }
    return obj;
}

浅克隆/浅拷贝

var shallowClone = function shallowClone(obj){
    //如果是基本数据类型值,则传啥就返回啥
    let type = toType(obj);
    if(/^(number|string|boolean|null|undefined|symbol|bigint)$/.test(type)) return obj;
    if(type === 'function') {
        //返回一个不同函数,但最后执行效果跟原始函数一致
        return function proxy(){
            obj();
        }
    }
    //if(type === 'regexp') return new RegExp(obj);
    //if(type === 'date') return new Date(obj);
    if(/^(regexp|date)$/.test(type)) return new obj.constructor(obj);
    if(type === 'error') return new Error(obj.message);

    let keys =  [
	...Object.keys(obj),
	...Object.getOwnPropertySymbols(obj)
    ];
    let clone = {};
    Array.isArray(obj) ? clone = [] : null;
    keys.forEach(item => {
        clone[item] = obj[item];
    });
    return clone;
}

深克隆/深拷贝

var deepClone = function deepClone(obj, cache = new Set()){		
    let type = toType(obj);
    //如果不是数组或对象则直接按浅克隆处理
    if(!/^(array|object)$/.test(type)) return shallowClone(obj);

    let keys = [
	...Object.keys(obj),
	...Object.getOwnPropertySymbols(obj)
    ];
    let clone = {};
    Array.isArray(obj) ? clone = [] : null;
    if(cache.has(obj)) return obj;
    cache.add(obj);
    keys.forEach(item => {		
        clone[item] = deepClone(obj[item], cache);
    });
    return clone;
}

浅合并

var shallowMerge = function shallowMerge(obj1, obj2){
    var isPlain1 = isPlainObject(obj1);
    var isPlain2 = isPlainObject(obj2);
    //只要obj1不是对象,那么不管obj2是不是对象,都用obj2直接替换obj1
    if(!isPlain1) return obj2;
    //走到这一步时,说明obj1肯定是对象,那如果obj2不是对象,则还是以obj1为主
    if(!isPlain2) return obj1;
    //如果上面两个条件都不成立,那说明obj1和obj2肯定都是对象, 则遍历obj2 进行合并
    let keys = [
        ...Object.keys(obj2),
        ...Object.getOwnPropertySymbols(obj2)
    ]
    keys.forEach(function(key){
        obj1[key] = obj2[key];
    });

    return obj1;
}

深合并

var deepMerge = function deepMerge(obj1, obj2){
    var isPlain1 = isPlainObject(obj1);
    var isPlain2 = isPlainObject(obj2);
    //obj1或obj2中只要其中一个不是对象,则按照浅合并的规则进行合并
    if(!isPlain1 || !isPlain2) return shallowMerge(obj1, obj2);
    //如果都是对象,则进行每一层级的递归合并
    let keys = [
        ...Object.keys(obj2),
        ...Object.getOwnPropertySymbols(obj2)
    ]
    keys.forEach(function(key){
        obj1[key] =  deepMerge(obj1[key], obj2[key]);//这里递归调用
    });

    return obj1;
}

防抖

//默认识别第一次,就是在该段时间内,只有第一次点击生效。
function debounce(func, wait = 300, immediate = true){
    let timer = null;
    return function anonymouse(...params){
        //每次点击都要先清除定时器重新计时wait
        clearTimeout(timer);
        //只要timer不为null,则说明是频繁点击,是频繁点击就需要重新计算并且函数func只需执行一次
        //一旦频繁点击结束,等待时间wait到了,则timer又会被重新置为null
        let now = immediate && !timer;
        timer = setTimeout(() => {
            //等待时间到了以后需要重新将timer置为null,便于识别是否是下一轮频繁点击的第一次点击
            timer = null;
            //这里之所以用这种方式调用func,是不想将原函数(func)中的this指向改变
            //当前这个匿名函数时由按钮触发,所以这里this指向的就是触发事件的那个按钮。
            //而如果不用函数的防抖机制,直接把函数func绑定给按钮,则在func中的this指向也是触发事件的按钮。
            //所以这里如果直接调用func函数的话,func中的this指向就变为window了。
            //如果immediate为true则在第一次触发就已经执行,这里等待时间到就不再执行了
            !immediate ? func.call(this, ...params) : null;
        },wait);

        //如果immediate为true说明是需要识别第一次触发,则调用函数执行
        now ? func.call(this, ...params) : null;
    }
}

节流

function throttole(func, wait){
    let timer = null,
        previous = 0;
    return function anonmouse(...params){
        let now = new Date(),
            remaining = wait - (now - previous);
        if(remaining <= 0){//两次执行时间已经超过设置的间隔时间了,可以执行了
            //将当前执行时间作为上次执行时间赋值给previous
            previous = now;
            //取消倒计时,因为这里已经执行了
            clearTimeout(timer);
            //执行完成后清空timer,便于下次设置倒计时定时器
            timer = null;
            func.call(this, ...params);
        }else if(!timer){
            //两次执行时间差还不到预设的间隔时间,并且也没有设置定时器倒计时;
            //则设置定时器开始倒计时,时间结束就又可以执行了
            //如果已经设置了定时器,就不再做任何处理了。
            timer = setTimeout(() => {
                //执行完成后清空timer,便于下次设置倒计时定时器
                timer = null;
                //将当前执行时间作为上次执行时间赋值给previous
                previous = new Date();				
                func.call(this, ...params);
            }, remaining);
        }
    }
}

惰性函数( 非通用,仅提供思路)

所谓的惰性函数,顾名思义就是懒函数,那到底是怎么个懒法呢,来看一下下面的场景:
小A在项目开发中经常需要去获取页面中元素的样式,同时还要考虑兼容一些老版本的浏览器,于是小A决定自己封装> 一个函数,专门用来获取不同元素的不同样式。

function getCss(element, attr){
	//浏览器兼容性判断
	if('getComputedStyle' in window){//或者用 !window.getComputedStyle
		return window.getComputedStyle(element)[attr];
	}else{
		return element.currentStyle[attr];
	}
}
var bodyBackground = getCss(document.body, 'background');
var bodyHeight= getCss(document.body, 'height');
var bodywidth= getCss(document.body, 'width');

小A 很开心,但用着用着,小A发现:虽然获取元素样式被封装成了方法也能兼容不同版本的浏览器,但是如果需要一次获取多个样式,那么就需要多次调用getCss函数,同时就需要多次进行兼容性判断,然而这期间并没有切换浏览器 ,这样的话其实判断一次就可以了,不需要每次都判断。于是小A就想:如果第一次执行完getCss函数也判断出了浏览器兼容性,那在不改变调用代码的情况下有没有什么办法可以让getCss的代码体变一下呢?于是:

function getCss(element, attr){
    //浏览器兼容性判断
    if('getComputedStyle' in window){//或者用 !window.getComputedStyle
        getCss = function(element, attr){
            return window.getComputedStyle(element)[attr];
        } 
    }else{
        getCss = function(element, attr){
            return element.currentStyle[attr];		
        }
    }
    //第一次调用时需要将结果返回
    return getCss(element, attr);
}
var bodyBackground = getCss(document.body, 'background');
var bodyHeight= getCss(document.body, 'height');
var bodywidth= getCss(document.body, 'width');

这样在第一次调用getCss时会进行兼容性判断,那么不管是否兼容,getCss都会重新指向另一个小函数,在这个小函数中就不再进行兼容性判断了,而且后面再去调用getCss时也就是直接调用了getCss的新的指向(小函数)。这就是所谓的惰性函数思想。

柯里化函数(非通用,仅提供思路)

所谓的柯理化函数:是一种预先处理的思想,那什么又是预先处理呢?其实就是利用闭包机制,把一些信息预先存储起来,以后基于作用域链,访问到事先存储的信息,然后进行相关的处理。所有符合这种模式(或闭包应用的)都被称为柯理化函数。

function carring(x){
	return function(...args){
		//args是一个数组,首先我们需要将预先保存的x的值添加到数组中,然后再对数组进行求和
		args.unshift(x);
		//数组求和 - 循环
		var total = 0;
		args.forEach(function(item, index){
			total += item;
		});		
		//数组求和 - reduce
		total = args.reduce(function(result, item, index){
			return result + item;
		})
	}
}
var b = a(10);
var total = b(20,30); // '10+20+30'
console.log(total);
total = b(20,30, 40);//'10+20+30+40'
console.log(total);

组合函数

在函数式编程中一个很重要的概念就是函数组合,实际上就是把处理数据的函数像管道一样连接起来,然后让数据穿过管道得到最终的结果。或者可以理解为:将多个函数组合成一个函数同时完成数据的传递

//funcs:存储的是最后需要执行的函数及顺序(最后传递的函数最先执行)
// 执行compose只是把最后要执行的函数及顺序预先保存起来,函数还没有执行(柯理化思想)
// 返回一个operate处理函数, 执行operate,并且传递初始值,然后按照之前存储的函数及顺序依次执行
function compose(...funcs){
    return function operate(x){
        //如果没有传递任何函数,则直接将原来的值x返回
        if(funcs.length === 0) return x;
        //如果只传递了一个参数,并且该参数是一个函数,则调用该函数并返回结果
        if(funcs.length === 1) return typeof funcs[0] === 'function' ? funcs[0](x) : x;
        //从右向左遍历
        return funcs.reduceRight(function(result, item, index){
            //如果item不是一个函数,则直接将上一次结果返回
            if(typeof item !== 'function') return  result;
            return item(result);//把得到的结果返回给下一次的result
        }, x);//传递了初始值,则第一次遍历result就是x,item则是第一个函数
    }
}

var add = x => x + 10;
var sub = x => x - 3;
var mul = x => x * 8;
var div = x => x / 2;

let res = compose(div,mul,sub,add)(5)
console.log(res);//48