数据类型检测
//建立数据类型检测映射表
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