使用思维导图来对new、instanceof、Object.create()、Object.assign()、map()、filter()、reduce()、flat()、call()、apply()、bind()、防抖、节流、深拷贝的实现原理进行阐述,然后利用js代码进行实现,为前端切图仔在求职工作中再添一门武功秘籍,提升自身内功。本节为第一节,后面将继续探索Promise、Async、Axios、发布订阅等的实现,请各位大佬关注指正。
一、new
function New (Fn, ...arg) { // 一个新的对象被创建 const result = {}; // 该对象的__proto__属性指向该构造函数的原型 if (Fn.prototype !== null) { Object.setPrototypeOf(result, Fn.prototype); } // 将执行上下文(this)绑定到新创建的对象中 const returnResult = Fn.apply(result, arg); // 如果构造函数有返回值,那么这个返回值将取代第一步中新创建的对象。否则返回该对象 if ((typeof returnResult === "object" || typeof returnResult === "function") && returnResult !== null) { return returnResult; } return result; }
二、instanceof
function Instanceof(left, right) { let leftVal = Object.getPrototypeOf(left); const rightVal = right.prototype; while (leftVal !== null) { if (leftVal === rightVal) return true; leftVal = Object.getPrototypeOf(leftVal); } return false; }
三、Object
Object上有很多静态方法,本次只实现Object.create()和Object.assign(),有兴趣的可以下载思维导图进行完善。
3.1 Object.create()
Object.ObjectCreate = (proto, propertiesObject)=> { // 对输入进行检测 if (typeof proto !== 'object' && typeof proto !== 'function' && proto !== null) { throw new Error(`Object prototype may only be an Object or null:${proto}`); } // 新建一个对象 const result = {}; // 将该对象的原型设置为proto Object.setPrototypeOf(result, proto); // 将属性赋值给该对象 Object.defineProperties(result, propertiesObject); // 返回该对象 return result; }
3.2 Object assign()
function ObjectAssign(target, ...sources) { // 对第一个参数的判断,不能为undefined和null if (target === undefined || target === null) { throw new TypeError('cannot convert first argument to object'); } // 将第一个参数转换为对象(不是对象转换为对象) const targetObj = Object(target); // 将源对象(source)自身的所有可枚举属性复制到目标对象(target) for (let i = 0; i < sources.length; i++) { let source = sources[i]; // 对于undefined和null在源角色中不会报错,会直接跳过 if (source !== undefined && source !== null) { // 将源角色转换成对象 // 需要将源角色自身的可枚举属性(包含Symbol值的属性)进行复制 // Reflect.ownKeys(obj) 返回一个数组,包含对象自身的所有属性,不管属性名是Symbol还是字符串,也不管是否可枚举 const keysArray = Reflect.ownKeys(Object(source)); for (let nextIndex = 0; nextIndex < keysArray.length; nextIndex ++) { const nextKey = keysArray[nextIndex]; // 去除不可枚举属性 const desc = Object.getOwnPropertyDescriptor(source, nextKey); if (desc !== undefined && desc.enumerable) { // 后面的属性会覆盖前面的属性 targetObj[nextKey] = source[nextKey]; } } } } return targetObj; } // 由于挂载到Object的assign是不可枚举的,直接挂载上去是可枚举的,所以采用这种方式 if (typeof Object.myAssign !== 'function') { Object.defineProperty(Object, "myAssign", { value : ObjectAssign, writable: true, enumerable: false, configurable: true }); }
四、数组原理
数组有很多方法,我们此处只实现了比较常见的map()、filter()、reduce()、flat(),有兴趣的童鞋可以继续补充。
4.1 map
Array.prototype.myMap = function(fn) { // 判断输入的第一个参数是不是函数 if (typeof fn !== 'function') { throw new TypeError(fn + 'is not a function'); } // 获取需要处理的数组内容 const arr = this; const len = arr.length; // 新建一个空数组用于装载新的内容 const temp = new Array(len); // 对数组中每个值进行处理 for (let i = 0; i < len; i++) { // 获取第二个参数,改变this指向 let result = fn.call(arguments[1], arr[i], i, arr); temp[i] = result; } // 返回新的结果 return temp; }
4.2 filter
Array.prototype.myFilter = function (fn) { if (typeof fn !== 'function') { throw new TypeError(`${fn} is not a function`); } // 获取该数组 const arr = this; // 获取该数组长度 const len = this.length >>> 0; // 新建一个新的数组用于放置该内容 const temp = []; // 对数组中每个值进行处理 for (let i = 0; i < len; i++) { // 处理时注意this指向 const result = fn.call(arguments[1], arr[i], i, arr); result && temp.push(arr[i]); } return temp; }
4.3 reduce
Array.prototype.myReduce = function(fn) { if (typeof fn !== 'function') { throw new TypeError(`${fn} is not a function`); } const arr = this; const len = arr.length >>> 0; let value;// 最终返回的值 let k = 0;// 当前索引 if (arguments.length >= 2) { value = arguments[1]; } else { // 当数组为稀疏数组时,判断数组当前是否有元素,如果没有索引加一 while (k < len && !( k in arr)) { k++; } // 如果数组为空且初始值不存在则报错 if (k >= len) { throw new TypeError('Reduce of empty array with no initial value'); } value = arr[k++]; } while (k < len) { if (k in arr) { value = fn(value, arr[k], k, arr); } k++; } return value; }
4.4 flat
// 使用reduce和concat Array.prototype.flat1 = function () { return this.reduce((acc, val) => acc.concat(val), []); }
// 使用reduce + concat + isArray +recursivity Array.prototype.flat2 = function (deep = 1) { const flatDeep = (arr, deep = 1) => { // return arr.reduce((acc, val) => Array.isArray(val) && deep > 0 ? [...acc, ...flatDeep(val, deep - 1)] : [...acc, val], []); return deep > 0 ? arr.reduce((acc, val) => acc.concat(Array.isArray(val) ? flatDeep(val, deep - 1) : val), []) : arr.slice(); } return flatDeep(this, deep); }
// 使用forEach + concat + isArray +recursivity // forEach 遍历数组会自动跳过空元素 Array.prototype.flat3 = function (deep = 1) { const result = []; (function flat(arr, deep) { arr.forEach((item) => { if (Array.isArray(item) && deep > 0) { flat(item, deep - 1); } else { result.push(item); } }) })(this, deep); return result; }
// 使用for of + concat + isArray +recursivity // for of 遍历数组会自动跳过空元素 Array.prototype.flat4 = function (deep = 1) { const result = []; (function flat(arr, deep) { for(let item of arr) { if (Array.isArray(item) && deep > 0) { flat(item, deep - 1); } else { // 去除空元素,因为void 表达式返回的都是undefined,不适用undefined是因为undefined在局部变量会被重写 item !== void 0 && result.push(item); } } })(this, deep); return result; }
// 使用堆栈stack Array.prototype.flat5 = function(deep = 1) { const stack = [...this]; const result = []; while (stack.length > 0) { const next = stack.pop(); if (Array.isArray(next)) { stack.push(...next); } else { result.push(next); } } // 反转恢复原来顺序 return result.reverse(); }
五、改变this指向
js中有三种方式改变this指向,分别是call、apply和bind。
5.1 call
Function.prototype.call1 = function(context, ...args) { // 获取第一个参数(注意第一个参数为null或undefined,this指向window),构建对象 context = context ? Object(context) : window; // 将对应函数传入该对象中 context.fn = this; // 获取参数并执行相应函数 let result = context.fn(...args); delete context.fn;
5.2 apply
Function.prototype.apply1 = function(context, arr) { context = context ? Object(context) : window; context.fn = this; let result = arr ? context.fn(...arr) : context.fn(); delete context.fn; return result; }
5.3 bind
Function.prototype.bind1 = function (context, ...args) { if (typeof this !== 'function') { throw new TypeError('The bound object needs to be a function'); } const self = this; const fNOP = function() {}; const fBound = function(...fBoundArgs) { // 指定this // 当作为构造函数时,this 指向实例,此时 this instanceof fBound 结果为 true return self.apply(this instanceof fNOP ? this : context, [...args, ...fBoundArgs]); } // 修改返回函数的 prototype 为绑定函数的 prototype,为了避免直接修改this的原型,所以新建了一个fNOP函数作为中介 if (this.prototype) { fNOP.prototype = this.prototype; } fBound.prototype = new fNOP(); return fBound; }
六、优化
防抖与节流函数是一种最常用的 高频触发优化方式,能对性能有较大的帮助。
6.1 防抖
function debounce(fn, wait, immediate) { let timer = null; return function(...args) { // 立即执行的功能(timer为空表示首次触发) if (immediate && !timer) { fn.apply(this, args); } // 有新的触发,则把定时器清空 timer && clearTimeout(timer); // 重新计时 timer = setTimeout(() => { fn.apply(this, args); }, wait) } }
6.2 节流
// 时间戳版本 function throttle(fn, wait) { // 上一次执行时间 let previous = 0; return function(...args) { // 当前时间 let now = +new Date(); if (now - previous > wait) { previous = now; fn.apply(this, args); } } }
// 定时器版本 function throttle(fn, wait) { let timer = null; return function(...args) { if (!timer) { timer = setTimeout(() => { fn.apply(this, args); timer = null; }, wait) } } }
七、深拷贝
// 乞巧版 function cloneDeep1(source) { return JSON.parse(JSON.stringify(source)); }
// 递归版 function cloneDeep2(source) { // 如果输入的为基本类型,直接返回 if (!(typeof source === 'object' && source !== null)) { return source; } // 判断输入的为数组函数对象,进行相应的构建 const target = Array.isArray(source) ? [] : {}; for (let key in source) { // 判断是否是自身属性 if (Object.prototype.hasOwnProperty.call(source, key)) { if (typeof source === 'object' && source !== null) { target[key] = cloneDeep2(source[key]); } else { target[key] = source[key]; } } } return target; }
// 循环方式 function cloneDeep3(source) { if (!(typeof source === 'object' && source !== null)) { return source; } const root = Array.isArray(source) ? [] : {}; // 定义一个栈 const loopList = [{ parent: root, key: undefined, data: source, }]; while (loopList.length > 0) { // 深度优先 const node = loopList.pop(); const parent = node.parent; const key = node.key; const data = node.data; // 初始化赋值目标,key为undefined则拷贝到父元素,否则拷贝到子元素 let res = parent; if (typeof key !== 'undefined') { res = parent[key] = Array.isArray(data) ? [] : {}; } for (let key in data) { if (data.hasOwnProperty(key)) { if (typeof data[key] === 'object' && data !== null) { loopList.push({ parent: res, key: key, data: data[key], }); } else { res[key] = data[key]; } } } } return root; }