「前端面试必会」手写篇|持续更新
都是自己以前总结的知识点,复习一下顺便发出来加深印象,仅供参考有错误感谢指出。
1. 手写简单Symbol
主要是实现了 Symbol 的几个特征,用对象模拟Symbol,但是并不完善
function SymbolPollify(description) { // 不能用 new 构造 if(new.target !== SymbolPollify) { throw new Error("Symbol 不能用 new 构造") } let symbol = Object.create({ toString() { return `Symbol(${this.__description__})` } }) // 无论传入什么 description 都转化为 字符串 let __description__ = String(description); Object.defineProperty(symbol, "__description__", { value: __description__, writable: false, configurable: false, enumerable: false }); return symbol; } let symbol_ = new SymbolPollify("111"); console.log(symbol_.toString());2. 用 Object.prototype.toString 封装一个通用 type 检测数据类型的工具
let obj2type = {}; let types = 'Number BigInt String Boolean Null Undefined Symbol Object Array Date Error RegExp Function'; types.split(' ').forEach(item => { obj2type[`[object ${item}]`] = item.toLowerCase(); }) // typeof 能直接判断的用typeof 否则用 toString 做 key let TypeChecker = type => typeof type !== 'object' ? typeof type : obj2type[Object.prototype.toString.call(type)] // test console.log(TypeChecker(123)); // number console.log(TypeChecker([1, 2, 3])); // array console.log(TypeChecker(() => {})); // function3. 手写一个 instanceof 操作符
function InstanceofPollify(left, right) { const isObject = typeof left === 'object' || typeof left === 'function'; // 左操作符是基本数据类型,直接返回 false if(!isObject || left === null) return false; if(typeof right !== 'function') throw new Error('右操作法必须是可调用的函数'); // 考虑 右操作符 有 [Symbol.hasInstance] 的情况,只适用于 class 类 if(right.hasOwnProperty(Symbol.hasInstance)) { return right[Symbol.hasInstance](left); } let proto = Object.getPrototypeOf(left); while(proto) { if(proto === right.prototype) return true; if(proto === null) return false; proto = Object.getPrototypeOf(left); } }
// 对于 instanceof 操作法无法检测基本操作类型的 缺陷 var a = 1; console.log(a instanceof Number); // false // 利用 [Symbol.hasInstance] 属性 class PrimitiveNumber { static [Symbol.hasInstance](left) { return typeof left === 'number'; } } console.log(a instanceof PrimitiveNumber); // false4. 手写深拷贝
const toBeCloneObj = { firstname: '飞行员', lastname: 'Pilot', address: { province: 'Beijing', attachnumber: [1, { number: 1 }, 0, {testmap: new Map(), testset: new Set()}], } } toBeCloneObj.address.myself = toBeCloneObj; // 循环引用 // 最简单版 区分普通对象和 Array function deepClone(sourceObj) { if(typeof sourceObj === 'object' && sourceObj !== null) { let cloneObj = Array.isArray(sourceObj) ? [] : {}; for(let key in sourceObj) { if(sourceObj.hasOwnProperty(key)) { cloneObj[key] = deepClone(sourceObj[key]); } } return cloneObj; } else return sourceObj; } // v2: 解决循环引用问题 function deepCloneV2(sourceObj, map = new WeakMap()) { if(typeof sourceObj === 'object' && sourceObj !== null) { let cloneObj = Array.isArray(sourceObj) ? [] : {}; if(map.get(sourceObj)) return map.get(sourceObj); map.set(sourceObj, cloneObj); for(let key in sourceObj) { if(sourceObj.hasOwnProperty(key)) { cloneObj[key] = deepCloneV2(sourceObj[key], map); } } return cloneObj; } else return sourceObj; } // v3:解决常见的如 Set Map 数据结构问题 const MapType = '[object Map]'; const SetType = '[object Set]'; const isObject = obj => typeof obj === 'object' && obj !== null; const Type = obj => Object.prototype.toString.call(obj); function deepCloneV3(sourceObj, map = new WeakMap()) { if(isObject(sourceObj)) { const ctor = sourceObj.constructor; let cloneObj = new ctor(); if(map.get(sourceObj)) return map.get(sourceObj); map.set(sourceObj, cloneObj); if(Type(cloneObj) === MapType) { for([key, value] of sourceObj) cloneObj.set(key, value) } if(Type(cloneObj) === SetType) { for(value of sourceObj) cloneObj.add(value) } for(let key in sourceObj) { if(sourceObj.hasOwnProperty(key)) { cloneObj[key] = deepCloneV3(sourceObj[key], map); } } return cloneObj; } else return sourceObj; } console.log(deepCloneV3(toBeCloneObj));5. 手写一个 new 操作符
// new 是操作符,所以我们只能够用函数来模拟 function objectFatory() { var obj = new Object(); // 第一个参数是构造函数,其余参数要继续参与运算 shift一举两得 var Constructor = [].shift.apply(arguments); if(typeof Constructor !== 'function'){ throw 'newOperator function the first param must be a function'; } // 设置 obj 的 __proto__ 指向 Constructor.prototype Object.setPrototypeOf(obj, Constructor.prototype); // new 的返回值不是 引用值,则返回 obj // 继承方法。借用构造函数(经典继承) var ret = Constructor.apply(obj, arguments); // 必须为对象,考虑isObject 考虑 typeof的特殊性 函数会返回 function let isObject = typeof ret === "object" && ret !== null; let isFunction = typeof ret === "function"; return isObject || isFunction ? ret : obj; }6. 实现 Object.create
Object.creat 和 new 的区别在于前者只是将对象的原型链连接好,并不是执行构造函数。
// 手动实现一个 Object.create const _create = function (o) { let F = function () {} F.prototype = o return new F() }7. 手写 apply call bind
/** * 所以我们模拟的步骤可以分为: * 将函数设为对象的属性,若考虑对象属性可能会重写,可以用Symbol * 执行该函数 * 删除该函数 */ Function.prototype.myCall = function (context) { // 非严格模式下 // null 和 undefined 指向全局对象,原始值进行 Object 操作顺便可以装箱 context = context == null ? globalThis : Object(context); context.fun = this; // 可以用 Symbol 做属性名 let args = []; // 将 arguments 类数组转换为数组 for (let i = 1; i < arguments.length; i++) { args.push(arguments[i]); } let result = context.fun(...args); delete context.fun; return result; }; function a(name, age) { console.log(this.a); console.log(name); console.log(age); } var obj = { a: "dwad", }; a.myCall(obj, "ddd", 18);
/** * 所以我们模拟的步骤可以分为: * 将函数设为对象的属性,若考虑对象属性可能会重写,可以用Symbol * 执行该函数 * 删除该函数 */ Function.prototype.myApply = function (context, args) { //null 和 undefined 指向全局对象,否则进行 Object 操作顺便可以装箱 context = context == null ? globalThis : Object(context); context.fun = this; // 可以用 Symbol 做属性名 let result = undefined; if (!args) { result = context.fun(); } else { result = context.fun(args); } delete context.fun; return result; }; function a(obj) { console.log(this.a); console.log(obj); } var obj = { a: "dwad", }; a.myApply(obj, ["ddd", 18]);
/** * bind 函数的特点: * 1. 返回一个函数 * 2. 可以给函数预定传入参数,别的参数依次排到预定参数后面 * 难点 3. bind 后的函数可以用 new 操作符当做构造函数使用,但是绑定的this * 值会失效(因为new操作符的原因,this会指向新生成的对象),但预先传递的参数会生效 */ // 实现 1 2 Function.prototype.myBind = function (context) { var self = this; var args = Array.prototype.slice.call(arguments, 1); return function () { // 注意两个 arguments 不一样 // 这里 转为 args2 真正数组的原因是因为 concat的参数必须要数组或者值 var args2 = Array.prototype.slice.call(arguments); return self.apply(context == null ? globalThis : Object(context),args.concat(args2)); // args 要放在 args2 前面 }; }; // 实现 3 Function.prototype.bind2 = function (context) { var self = this; var args = Array.prototype.slice.call(arguments, 1); var fBound = function () { var bindArgs = Array.prototype.slice.call(arguments); // 当作为构造函数时,this 指向实例,此时结果为 true,将绑定函数的 this 指向该实例,可以让实例获得来自绑定函数的值 // 以上面的是 demo 为例,如果改成 `this instanceof fBound ? null : context`,实例只是一个空对象,将 null 改成 this ,实例会具有 habit 属性 // 当作为普通函数时,this 指向 window,此时结果为 false,将绑定函数的 this 指向 context return self.apply( this instanceof fBound ? this : context, args.concat(bindArgs) ); }; // 修改返回函数的 prototype 为绑定函数的实例,实例就可以继承绑定函数的原型中的值 fBound.prototype = Object.create(this.prototype); return fBound; };