前端面试复习指南——手写代码

185 阅读4分钟

✨ call、apply、bind

call,apply,bind 都是为了改变函数运行时上下文(this指向)而存在的

  • 以上三个函数接收的第一个参数都是 要绑定的this指向.
  • apply的第二个参数是一个参数数组,callbind的第二个及之后的参数作为函数实参按顺序传入。
  • bind不会立即调用,其他两个会立即调用。

手写call

Function.prototype.myOwnCall = function(context, ...args) {
    context = context || window;
    let fn = Symbol('fn');
    context[fn] = this;

    const result =  thisArg[fn](...args);
    delete context[fn];
    return result;
}

手写apply

Function.prototype.myOwnApply = function(context, arr) {
    context = context || window
    let fn = Symbol('fn');
    context[fn] = this;

    var args = [];
    var result = null;

    if (!arr) {
    result = context[fn]();
    } else {
    result = context[fn](arr);
    }
    delete context[fn];
    return result;
}

手写bind

把新的 this 绑定到某个函数 func 上,并返回 func 的一个拷贝

let boundFunc = func.bind(thisArg[, arg1[, arg2[, ...argN]]])
  • 初级

✅ 使用 ES6 语法实现

❌ 不兼容 IE,不支持 new

// 初级:ES6 新语法 const/...
function bind_1(asThis, ...args) {
  const fn = this; // 这里的 this 就是调用 bind 的函数 func
  return function (...args2) {
    return fn.apply(asThis, ...args, ...args2);
  };
}
  • 中级

✅ 兼容 IE

❌ 不支持 new

function bind2(asThis) {
  var slice = Array.prototype.slice;
  var args = slice.call(arguments, 1);
  var fn = this;
  if (typeof fn !== "function") { // 加入了对调用函数类型的判断
    throw new Error("cannot bind non_function");
  }
  return function () {
    var args2 = slice.call(arguments, 0);
    return fn.apply(asThis, args.concat(args2));
  };
}
  • 高级
function bind3(thisArg, ...args) {
	const originFunc = this;
  const boundFunc = function (...args1) {
      // 解决 bind 之后对返回函数 new 的问题
      if (new.target) {
          if (originFunc.prototype) {
              boundFunc.prototype = originFunc.prototype;
          }
          const res = originFunc.apply(this, args.concat(args1));
          return res !== null && (typeof res === 'object' || typeof res === 'function') ? res : this;
      } else {
          return originFunc.apply(thisArg, args.concat(args1));
      }
  };
  // 解决length 和 name 属性问题
  const desc = Object.getOwnPropertyDescriptors(originFunc);
  Object.defineProperties(boundFunc, {
      length: Object.assign(desc.length, {
          value: desc.length < args.length ? 0 : (desc.length - args.length)
      }),
      name: Object.assign(desc.name, {
          value: `bound ${desc.name.value}`
      })
  });
  return boundFunc;
};

✨ 手写深拷贝

简单版

JSON反序列化

const B = JSON.parse(JSON.stringify(A))

❌ JSON value不支持的数据类型,都拷贝不了:

  1. 不支持函数
  2. 不支持undefined(支持null
  3. 不支持循环引用,比如 a = {name: 'a'}a.self = aa2 = JSON.parse(JSON.stringify(a))
  4. 不支持Date,会变成 ISO8601 格式的字符串
  5. 不支持正则表达式
  6. 不支持Symbol

复杂版

  1. 第一版

        function isObject(obj){
            return Object.prototype.toString.call(obj) === '[Object Object]';
        }
        function clone(source){
        if(!isObject(source)) return source;
        const target = {};
    
        for(let i in target) {
            if(source.hasOwnProperty(i)){
                if(isObject(source[i])){
                    target[i] = clone(source[i]);
                } else {
                    target[i] = source[i];
                }
            }
        }
    
        return target;
    }
    
    • 没有对参数做检验
    • 判断是否对象的逻辑不够严谨
    • 没有考虑数组的兼容
  2. 第二版

    function isObject(obj){
        return typeof obj === 'object' && obj != null; // 兼容数组
    }
    // 使用 循环检测 解决循环引用
    function clone(source, hash = new WeakMap()){
        if(!isObject(source)) return source;
        if (hash.has(source)) return hash.get(source); // 新增代码,查哈希表
    
        const target = Array.isArray(source) ? [...source] : {...source};
        hash.set(source, target); // 新增代码,哈希表设值
    
    	for(let i in target) {
                if(target.hasOwnProperty(i)){
                    if(isObject(target[i])){
                        target[i] = clone(target[i], hash); // 新增代码,传入哈希表
                    } 
                 }
            }
    	return target;
    }
    // 这种方法还可以解决引用丢失问题
    

    没有解决递归爆栈。

  3. 第三版

    将递归改为循环来破解爆栈

    function isObject(obj){
        return typeof obj === 'object' && obj != null; // 兼容数组
    }
    
    function clone(source, hash = new WeakMap()){
        const root = {};
    	// 栈
        const loopList = [{
            parent: root,
            key: undefined,
            data: source,
        }];
    
        while(loopList.length){
            // 深度优先
            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] = {};
            }
            // 数据已经存在
            if (hash.has(data)) {
                parent[key] = hash.get(data);
                continue; // 中断本次循环
            }
    
            // 数据不存在
            // 保存源数据,在拷贝数据中对应的引用
            hash.set(data, res);
    
    	for(let i in data) {
                if(data.hasOwnProperty(i)){
    		if(isObject(data[i])){
                      // 下一次循环
                      loopList.push({
                          parent: res,
                          key: i,
                          data: data[i],
                      });
                      } else {
                        res[k] = data[k];
                      }
                    }
                }
    	}
    	return root;
    }
    

✨ 手写防抖&节流

  • 防抖
    • 按钮提交场景:防止多次提交按钮,只执行最后提交的一次
    • 服务端验证场景:表单验证需要服务端配合,只执行一段连续的输入事件的最后一次,还有搜索联想词功能类似
  • 节流
    • 拖拽场景:固定时间内只执行一次,防止超高频次触发位置变动
    • 缩放/滚动场景:监控浏览器resize,监控scroll高度
    • 动画场景:避免短时间内多次触发动画引起性能问题
// 防抖函数 debounce
const debounce = (fn, delay = 500) => {
	let timer = null;
	return (...args) => {
		if (timer) clearTimeout(timer)
		timer = setTimeout(()=>{
			fn.apply(this, args);
		}, delay)
	}
}
// 进阶版 debounce
// leading 表示进入时是否立即执行
const debounce = function(fn, delay = 500, options: {leading: true, context: null}){
	let timer = null;
	let res;
	const _debounce = function (...args) {
		options.context || (options.context = this);  
		if(timer) clearTimeout(timer);
		if(options.leading && !timer){
			timer = setTimeout(()=>{timer = null}, delay);
			res = fn.apply(option.context, args);
		} else {
			timer = setTimeout(() => {
				res = fn.apply(option.context, args);
				timer = null;
			}, delay);
		}
		return res;
  }
	_debounce.cancle = function(){
		clearTimeout(timer);
		timer = null;
	}
	return _debounce;
}

// 节流函数 throttle
// fn 是需要执行的函数
// wait 是时间间隔
const throttle = (fn, wait = 50) => {
  // 上一次执行 fn 的时间
  let previous = 0
  // 将 throttle 处理结果当作函数返回
  return function(...args) {
    // 获取当前时间,转换成时间戳,单位毫秒
    let now = +new Date()
    // 将当前时间和上一次执行函数的时间进行对比
    // 大于等待时间就把 previous 设置为当前时间并执行函数 fn
    if (now - previous > wait) {
      previous = now
      fn.apply(this, args)
    }
  }
}

// 进阶版 throttle
// leading 表示进入时是否立即执行,trailing 表示是否在最后额外触发一次
const throttle = (fn, wait  = 50, options: {leading: true, trailing: false, context: null}){
	let previous = 0;
	let res = ;
	let timer;
	const _throttle = function(...args){
		options.context || (options.context = this);  
		let now = Date.now();
		if(!previous && !options.leading) previous = now;
		if(now - previous >= wait){
			if (timer) {
	        clearTimeout(timer);
	        timer = null;
      }
			res = fn.apply(options.context, args);
			previous = now;
		} else if(!timer && options.trailing){
			timer = setTimeout(()=>{
				res = fn.apply(options.context, args);
				previous = 0;
        timer = null;
			}, wait);
		}
		return res;
	}
	_throttle.cancel = function () {
      previous = 0;
      clearTimeout(timer);
      timer = null;
  };
  return _throttle;
}

// DEMO
// 执行 throttle 函数返回新函数
const betterFn = throttle(() => console.log('fn 函数执行了'), 1000)
// 每 10 毫秒执行一次 betterFn 函数,但是只有时间差大于 1000 时才会执行 fn
setInterval(betterFn, 10)

实现instanceOf

function instance_of(L, R) {
	let O = R.prototype;
	L = L.__proto__; // 取 L 的隐式原型,等同于 L = Object.getPrototypeOf(L);
	while(L){
		if(L === O) return true;
		L = L.__proto__;
	}
	return false;
}

//   等同于
function instance_of (L, R) {
 return right.prototype.isPrototypeOf(L);
};

实现new

new 执行过程如下:

  1. 创建一个新对象;
  2. 新对象的[[prototype]]特性指向构造函数的prototype属性;
  3. 构造函数内部的this指向新对象;
  4. 执行构造函数;
  5. 如果构造函数返回非空对象,则返回该对象;否则,返回刚创建的新对象
const myOwnNew = (constructor, ...args) => {
	let target = {}

	target.__proto__ = constructor.prototype;  // 等同于 target = Object.create(constructor.prototype);
	const res = constructor.apply(target, args)

	return res instanceof Object ? res : target;
}
const a = function(){
	return 'a';
}
const a1 = myOwnNew(a);

实现Object.create

function create(obj){
    function f(){};
    f.proptotype = obj;
    return new f();
}

实现Object.is

Object.is() 和 === 的区别是 Object.is(0, -0) 返回 false, Object.is(NaN, NaN) 返回 true。

// polyfill
const iIs = function (x, y) {
    if (x === y) {
        return x !== 0 || 1 / x === 1 / y;  // 0, -0
    } else {
        return x !== x && y !== y;  // NaN
    }
}

实现flat

// reduce + 递归
function flat(arr, num = 1) {
  return num > 0
    ? arr.reduce(
        (pre, cur) =>
          pre.concat(Array.isArray(cur) ? flat(cur, num - 1) : cur),
        []
      )
    : arr.slice();
}
const arr = [1, 2, 3, 4, [1, 2, 3, [1, 2, 3, [1, 2, 3]]], 5, "string", { name: "弹铁蛋同学" }]
flat(arr, Infinity);
// [1, 2, 3, 4, 1, 2, 3, 1, 2, 3, 1, 2, 3, 5, "string", { name: "弹铁蛋同学" }];

函数柯里化

将一个多参数函数转化为多个嵌套的单参数函数。

const curry = function (targetFn) {
 return function fn (...rest) {
     if (targetFn.length === rest.length) {
            return targetFn.apply(null, rest);
        }  else {
            return fn.bind(null, ...rest);
        }
    };
};
// 用法
function add (a, b, c, d) {
    return a + b + c + d;
}
console.log('柯里化:', curry(add)(1)(2)(3)(4)); 
// 柯里化:10