【手撕】写给中高级前端的 27 道手写题,希望能乘风破浪

2,240 阅读15分钟

这是我参与8月更文挑战的第1天,活动详情查看:8月更文挑战

前言

手写题一直是我的痛点,既然是痛点,那就要去永攀高峰,虽然我也想在哪跌倒就在哪趴着,但是内卷君告诉我:不!你不能! 接下来是一些我总结,还有一些我以前文章中写的内容,进行一次大集合,希望能帮到各位,希望能多多提出意见。

去重

方法一:利用 set

const res = [...new Set(arr)]

方法二:两层 for 循环 + splice

const unique = arr => {
  const len = arr.length;
  for (let i = 0; i < len; i++) {
  	for (let j = i + 1; j < len; j++) {
    	if (arr[i] === arr[j]) {
      	arr.splice(j ,1);
        len--;
        j--;
      }
    }
  }
  return arr;
}

方法三:利用 indexOf

const unique = arr => {
  let res = [];
  for (let i = 0; i < arr.length; i++) {
  	if (res.indexOf(arr[i]) === -1) {
    	res.push(arr[i]);
    }
  }
  return res;
}

方法四:利用 reduce

const unique = arr => {
	return arr.reduce((acc, cur) => {
  	if (acc.indexOf(cur) === -1) {
    	acc.push(cur);
    }
    return acc;
  }, [])
}

方法五:利用 Map 字典

const unique = arr => {
  let map = new Map();
  for (const item of arr) {
  	if (!map.has(item)) {
    	map.set(item, item);
    }
  }
  return Array.from(map.values());
}

实现 new 关键词

  1. 创建一个新对象
  2. 将新对象的 proto指向构造函数的 prototype,这个新对象就可以访问构造函数原型上的属性
  3. 将 this 指向改变,指向新的对象,这样就可以访问构造函数内部的属性
  4. 返回新的对象
function MyNew () {
  let obj = new Object();
  let Constructor = [].slice.call(arguments);
  obj.__proto__ = Constructor.prototype;
  let res = Constructor.apply(obj, arguments);
  return typeof res === 'object' ? res : obj;
}

数组扁平化

数组扁平化是指将一个多维数组变为一个一维数组

const arr = [1, [2, [3, [4, 5]]], 6];
// [1, 2, 3, 4, 5, 6]

方法一:使用 flat

const res = arr.flat(Infinity)

方法二:使用 reduce

const flatten = arr => {
    return arr.reduce((acc, cur) => {
    if (Array.isArray(cur)) {
    acc = [...acc, ...flatten(cur)]
    } else {
    	acc.push(cur);
    }
    return acc;
  }, [])
}

方法三:递归

const flatten = arr => {
  let res = [];
  for (let i = 0; i < arr.length; i++) {
  	if (Array.isArray(arr[i])) {
    	res = [...res, ...flatten(arr[i])]
    } else {
    	res = [...res, arr[i]];
    }
  }
  return res;
}

实现数组的方法

Array.prototype.filter

image.png

  1. 首先进行类型判断
  2. 因为返回一个新数组,所以要新建一个空数组保存其结果
  3. 将 this 强制转换成对象
  4. >>> 0 保证 len 为 number,且为正整数
  5. 遍历对象,检查 i 是否在 O 的属性(会检查原型链),回调函数调用传参
Array.prototype.filter = function(callback, thisArg) {
  if (this == null) {
    throw new TypeError('this is null or not defined');
  }
  if (typeof callback !== 'function') {
    throw new TypeError(callback + 'is not a function');
  }
  let res = [];
  let O = Object(this);
  let len = O.length >>> 0;
  for (let i = 0; i < O.length; i++) {
  	if (i in O) {
    	if (callback.call(thisArg, O[i], i, O)) {
      	res.push(O[i]);
      }
    }
  } 
  return res;
}

Array.prototype.map

image.png

  1. 类型判断
  2. 由于返回一个新数组,所以要定义一个空数组用于返回
  3. 将 this 强制转换为对象
  4. >>> 0 保证 len 为 number,且为正整数
  5. 遍历对象,判断 i 是否创业板在 O 的属性(会检查原型链),回调函数调用传参,并将返回值存入到新数组中
  6. 返回新数组
Array.prototype.map = function (callback, thisArg) {
   if (this == null) {
      throw new TypeError('this is null or not defined')
  }
  if (typeOf callback !== 'function') {
      throw new TypeError(callback + 'is not a function');
  }
  let res = [];
  let O = Object(this);
  let len = O.length >>> 0;
  for (let i = 0; i < len; i++) {
  	for (i in O) {
    	res[i] = callback.call(thisArg, O[i], i, O);
    }
  }
  return res;
}

Array.prototype.forEach

image.png

  1. 类型判断
  2. 将 this 强制转换为对象
  3. >>> 0 保证 len 为 number,并且为正整数
  4. 新建 k 作为下标
  5. 循环判断 k 是否小于 len,并且判断 k 是否为 O 的属性,调用回调函数,并自增 k
Array.prototype.forEach = function (callback, thisArg) {
   if (this == null) {
    throw new TypeError('this is null or not defined');
  }
  if (typeOf callback !== 'function') {
    throw new TypeError(callback + 'is not a function')
  }
  let O = Object(this);
  let len = O.length >>> 0;
  let k = 0;
  while (k < len) {
  	if (k in O) {
    	callback.call(thisArg, O[k], k, thisArg);
    }
    k++
  } 
}

Array.prototype.reduce

image.png

  1. 类型判断
  2. 将 this 强制转换为对象
  3. >>> 0 将 len 转换为 number,并且为正整数
  4. 将初始值 initialValue 赋值给变量 accumulator
  5. 定义 k 作为下标
  6. 当没有传入 initialValue 时,则数组的第一个有效值作为累加器的初始值
  7. 当传入 initialValue 时,则调用回调函数的返回值为累加值
  8. 最后返回累加值
Array.prototype.reduce = function (callback, initialValue) {
   if (this == null) {
    throw new TypeError('this is null or not defined');
  } 
  if (typeOf callback !== 'function') {
    throw new TypeError(callback + 'is not a function');
  }
  let O = Object(this);
  let len = O.length >>> 0;
  let accumulator = initialValue;
  let k = 0;
  if (accumulator === undefined) {
  	while (k < len && !(k in O)) {
    	k++;
    } 
    if (k >= len) {
    	throw new TypeError('Reduce of empty array with no initial value');
    }
    accumulator = O[k++];
  }
  while (k < len) {
  	if (k in O) {
    	accumulator = callback.call(undefined, accumulator, O[k], k, O);
    }
    k++;
  }
  return accumulator;
}

实现函数方法

Function.prototype.call

  1. 类型判断
  2. 当 this 为 null 时,默认走 window
  3. 将 Context 新增函数 fn,并将 this 指向到这个新的 fn
  4. 获取其他参数 args
  5. 将 args 传入 fn 函数中,保存返回值 res
  6. 删除 fn 函数
  7. 返回 res
Function.prototype.call = function(Context) {
  if (typeOf this !== 'function') {
    throw new TypeError('this is not a function');
  }
  let Context = Context || window;
  Context.fn = this;
  let args = [...arguments].slice(1);
  let res = Context.fn(...args);
  delete Context.fn;
  return res;
}

Function.prototype.apply

  1. 类型判断
  2. this 为 null,默认走 window
  3. 在 Context 中新建一个 fn 属性,将 this 指向 fn
  4. 截取 arguments,apply 与 call 的不同就是传参上,apply 传入一个数组
  5. 将 arguments 传入到 Context.fn 函数中
  6. 删除 fn 这个函数
  7. 返回 res
Function.prototype.apply = function (Context) {
  if (typeOf this !== 'function') {
    throw new TypeError('this is not a function');
  }
  let Context = Context || window;
  Context.fn = this;
  let args = arguments[1] ? arguments[1] : [];
  let res = Context.fn(...arguments);
  delete Context.fn;
  return res;
}

Function.prototype.bind

bind() 方法会创建一个新函数,当这个新函数被调用时,它的this值是传递给bind()的第一个参数, 它的参数是bind()的其他参数和其原本的参数。

  1. 返回一个新函数
  2. bind也是可以携带参数的,携带参数的方式和call相同:
  1. 可以在返回的函数中传入参数
  2. 绑定函数自动适应于使用 new 操作符去构造一个由目标函数创建的新实例。当一个绑定函数是用来构建一个值的,原来提供的 this 就会被忽略。不过提供的参数列表仍然会插入到构造函数调用时的参数列表之前。

模拟 bind 方法的步骤:

  1. 类型判断
  2. 将 this 指向进行缓存
  3. 截取 argumens,即除了第一个参数
  4. 新建一个空函数,用于创建实例
  5. 创建 bindFun 返回函数,由于返回函数可以传参的特性,并且可以和 bind 本身的参数进行合并
Function.prototype.bind = function(Context) {
  if (typeOf this !== 'function') {
    throw new TypeError('this is not a function');
  }
  let self = this;
  let args = [].slice.call(arguments, 1);
  let cacheFn = function () {};
  let bindFun =  function () {
    let bindArgs = [].arguments.call(arguments);
    return self.apply(this instanceof cacheFn ? this : Context, args.concat(bindArgs));
  }
  cacheFn.prototype = this.prototype;
  bindFun.prototype = new cacheFn();
  return bindFun;
}
  1. 为什么要判断this instanceof bindFun?

之前也说到,当将bind返回后函数当做构造函数时,bindFoo即是BindFoo的实例也是bar的实例,BindFoo即为返回来的函数,在我们模拟的代码中就是bindFun这个函数,并且当new之后this指向的是实例,所以用this instanceof bindFun判断的实际就是函数前有没有new这个关键词。

  1. 为什么要继承this的原型?

这是为了继承bar原型上的属性。 最后一步,健壮模拟的bind,判断传过来的this是否为函数

防抖

防抖的原理就是:你尽管触发事件,但是我一定在事件触发 n 秒后才执行,如果你在一个事件触发的 n 秒内又触发了这个事件,那我就以新的事件的时间为准,n 秒后才执行,总之,就是要等你触发完事件 n 秒内不再触发事件,我才执行

第一版

function debounce(func, wait) {
  let timeout;
  return function () {
    clearTimeout(timeout);
    timeout = setTimeout(func, wait);
  }
}

第二版

this 指向正确的对象

function debounce(func, wait) {
  let timeout;
  return function () {
    let context = this;
    clearTimeout(timeout);
    timeout = setTimeout(function () {
    	func.apply(context);
    }, wait);
  }
}

第三版

JavaScript 在事件处理函数中会提供事件对象 event

function debounce(func, wait) {
  let timeout;
  return function () {
    let context = this;
    let args = arguments;
    clearnTimout(timeout);
    timeout = setTimeout(function () {
    	func.apply(context, args);
    }, wait)
  }
}

第四版

返回值

function debounce(func, wait) {
  let timeout, res;
  return function () {
    let context = this;
    let args = arguments;
    clearTimeout(timeout);
    timeout = setTimeout(function () {
    	res = func.apply(context, args);
    }, wait)
    return res;
  }
}

第五版

立即执行

不希望非要等到事件停止触发后才执行,希望立刻执行函数,然后等到停止触发n秒后,才可以重新触发执行。

function debounce(func, wait, immediate) {
  let timeout, res;
  return function () {
    let context = this;
    let args = argumnets;
    if (timeout) clearTimeout(timeout);
    if (immediate) {
      let callNow = !timeout;
      timeout = setTimeout(function() {
      	timeout = null;
      }, wait);
      if (callNow) res = func.apply(context, args);
    } else {
    	timeout = setTimeout(function() {
      	res = func.apply(context, args);
      }, wait)
    }
    return res;
  }
}

第六版

希望能取消 debounce 函数,比如说我 debounce 的时间间隔是 10 秒钟,immediate 为 true,这样的话,我只有等 10 秒后才能重新触发事件,现在我希望有一个按钮,点击后,取消防抖,这样我再去触发,就可以又立刻执行

function debounce(func, wait, immediate) {
  let timeout, res;
  let debounced = function() {
    let context = this;
    let args = arguments;
    if (timeout) clearTimeout(timeout);
    if (immediate) {
      let callNow = !timeout;
      timeout = setTimeout(function() {
      	timeout = null;
      }, wait)
      if (callNow) res = func.apply(context, args);
    } else {
    	timeout = setTimeout(function() {
      	res = func.apply(context, args);
      }, wait)
    }
    return res;
  }
  debounced.cancel = function() {
    clearTimeout(timeout);
    timeout = null;
  }
  return debounced;
}

节流

如果你持续触发事件,每隔一段时间,只执行一次事件。

第一版

当触发事件的时候,我们设置一个定时器,再触发事件的时候,如果定时器存在,就不执行,直到定时器执行,然后执行函数,清空定时器,这样就可以设置下个定时器。

function throttle(func, wait) {
  let timeout;
  return function () {
    let context = this;
    let args = arguments;
    if (!timeout) {
    	timeout = setTimeout(function() {
      	timeout = null;
        func.apply(context, args);
      }, wait)
    }
  }
}

第二版

鼠标移入能立刻执行,停止触发的时候还能再执行一次!

function throttle(func, wait) {
    var timeout, context, args;
    var previous = 0;
    var later = function() {
        previous = +new Date();
        timeout = null;
        func.apply(context, args)
    };

    var throttled = function() {
        var now = +new Date();
        //下次触发 func 剩余的时间
        var remaining = wait - (now - previous);
        context = this;
        args = arguments;
         // 如果没有剩余的时间了或者你改了系统时间
        if (remaining <= 0 || remaining > wait) {
            if (timeout) {
                clearTimeout(timeout);
                timeout = null;
            }
            previous = now;
            func.apply(context, args);
        } else if (!timeout) {
            timeout = setTimeout(later, remaining);
        }
    };
    return throttled;
}

第三版

但是我有时也希望无头有尾,或者有头无尾,这个咋办?

那我们设置个 options 作为第三个参数,然后根据传的值判断到底哪种效果,我们约定:

leading:false 表示禁用第一次执行
trailing: false 表示禁用停止触发的回调

function throttle(func, wait, options) {
    var timeout, context, args;
    var previous = 0;
    if (!options) options = {};
    var later = function() {
        previous = options.leading === false ? 0 : new Date().getTime();
        timeout = null;
        func.apply(context, args);
        if (!timeout) context = args = null;
    };

    var throttled = function() {
        var now = new Date().getTime();
        if (!previous && options.leading === false) previous = now;
        var remaining = wait - (now - previous);
        context = this;
        args = arguments;
        if (remaining <= 0 || remaining > wait) {
            if (timeout) {
                clearTimeout(timeout);
                timeout = null;
            }
            previous = now;
            func.apply(context, args);
            if (!timeout) context = args = null;
        } else if (!timeout && options.trailing !== false) {
            timeout = setTimeout(later, remaining);
        }
    };
    return throttled;
}

第四版

取消

function throttle(func, wait, options) {
    var timeout, context, args;
    var previous = 0;
    if (!options) options = {};
    var later = function() {
        previous = options.leading === false ? 0 : new Date().getTime();
        timeout = null;
        func.apply(context, args);
        if (!timeout) context = args = null;
    };

    var throttled = function() {
        var now = new Date().getTime();
        if (!previous && options.leading === false) previous = now;
        var remaining = wait - (now - previous);
        context = this;
        args = arguments;
        if (remaining <= 0 || remaining > wait) {
            if (timeout) {
                clearTimeout(timeout);
                timeout = null;
            }
            previous = now;
            func.apply(context, args);
            if (!timeout) context = args = null;
        } else if (!timeout && options.trailing !== false) {
            timeout = setTimeout(later, remaining);
        }
    };
    throttled.cancel = function() {
      clearTimeout(timeout);
      previous = 0;
      timeout = null;
  	}
    return throttled;
}

实现 instanceof

instanceof 实际就是循环链表,在链表中查找与构造函数形同的原型,next 指针就是 proto

function instanceof (instance, contructor) {
  if (instance === null || typeof instance !== 'object' || typeof instance !== 'function') {
    return false;
  }  
  let proto = Object.getPrototypeOf(instance);
  while (proto !== null) {
  	if (proto === contructoe.prototype) {
    	return true;
    }
    proto = Object.getPrototypeOf(proto);
  }
  return false;
}

实现深拷贝

递归

第一版

简略版,只兼容了数组和对象

function clone(obj) {
    if (typeof obj !== 'object') {
    return obj;
  }
  let new_obj = Array.isArray(obj) ? [] : {};
  for (const key in obj) {
      if (obj.hasOwnProperty(key)) {
      new_obj[key] = clone(obj[key]);
    }
  }
  return new_obj;
}

第二版

解决循环引用

解决循环引用问题,我们可以额外开辟一个存储空间,来存储当前对象和拷贝对象的对应关系,当需要拷贝当前对象时,先去存储空间中找,有没有拷贝过这个对象,如果有的话直接返回,如果没有的话继续拷贝。

这个存储空间,需要可以存储key-value形式的数据,且key可以是一个引用类型,我们可以选择Map这种数据结构:

  1. 检查map中有无克隆过的对象
  2. 有 - 直接返回
  3. 没有 - 将当前对象作为key,克隆对象作为value进行存储
  4. 继续克隆
function clone(obj, map = new Map()) {
    if (typeof obj !== 'object') {
      return obj;
  }
  let new_obj = Array.isArray(obj) ? [] : {};
  if (map.has(obj)) {
      return map.get(obj);
  }
  map.set(obj, new_obj);
  for (const key in obj) {
    if (obj.hasOwnProperty(key)) {
      new_obj[key] = clone(obj[key], map);
    }
  }
  return new_obj;
}

第三版

运用弱引用,WeakMap

其实弱引用的好处就是可以在任何时刻被回收,提高性能,map 虽然可以进行手动释放提高性能,但是在某些情况下,是无法进行手动清除的。

function clone(obj, map = new WeakMap()) {
  if (typeof obj !== 'object') {
      return obj;
  }
  let new_obj = Array.isArray(obj) ? [] : {};
  if (map.has(obj)) {
    return map.get(obj);
  }
  map.set(obj, new_obj);
  for (const key in obj) {
  	if (obj.hasOwnProperty(key)) {
    	new_obj[key] = clone(obj[key], map);
    }
  }
  return new_obj;
}

第四版

其实上述代码就已经可以实现深拷贝了,但是为了兼容其他数据类型,我们还要进一步优化

兼容 set 和 map

// 判断是否为引用数据类型,并且判断是否为 function 和 null
const isObject = (obj) => {
  const type = typeof obj;
  return obj !== null && (type === 'object' || type === 'function')
}
// 获取类型
const getType = (obj) => {
    return Object.peototype.toString.call(obj);
}
// 获取初始值
const getInit = (obj) => {
  const Con = obj.contructor;
  return new Con();
}
const clone = (obj, map = new WeakMap()) => {
  if (!isObject(obj)) {
     return obj;
  }
  const type = getType(obj);
  let new_obj;
  const types = ["[object Map]", "[object Set]", "[object Array]", "[object Object]", "[object Arguments]"];
  if (types.includes(type)) {
  	new_obj = getInit(obj);
  }
  if (map.has(obj)) {
  	return map.get(obj);
  }
  map.set(obj, new_obj);
  // 克隆 set
  if (type === "[object Set]") {
  	obj.forEach((value) => {
    	new_obj.add(clone(value, map));
    })
  }
  // 克隆 map
  if (type === "[object Map]") {
  	obj.forEach((value, key) => {
    	new_obj.set(key, clone(value, map))
    })
  }
  // 克隆对象和数组
  if (type === "[object Object]" || type === "[object Array]") {
  	for (const key in obj) {
    	if (obj.hasOwnProperty(key)) {
      	new_obj[key] = clone(obj[key], map);
      }
    }
  }
  return new_obj;
}

第五版

克隆其他不可继续遍历的数据类型

// 类型
const mapTag = '[object Map]';
const setTag = '[object Set]';
const arrayTag = '[object Array]';
const objectTag = '[object Object]';
const argsTag = '[object Arguments]';

const boolTag = '[object Boolean]';
const dateTag = '[object Date]';
const numberTag = '[object Number]';
const stringTag = '[object String]';
const symbolTag = '[object Symbol]';
const errorTag = '[object Error]';
const regexpTag = '[object RegExp]';
const funcTag = '[object Function]';

const deepTag = [mapTag, setTag, arrayTag, objectTag, argsTag];

const cloneOtherType = (targe, type) => {
  const Con = targe.constructor;
  switch (type) {
    case boolTag:
    case numberTag:
    case stringTag:
    case errorTag:
    case dateTag:
      return new Con(targe);
    case regexpTag:
      return cloneReg(targe);
    case funcTag:
      return cloneSymbol(targe);
    case symbolTag:
      return cloneFunction(targe);  
    default:
      return null;
  }
};

// 克隆正则
const cloneReg = (target) => {
  const reFlags = /\w*$/;
  const result = new targe.constructor(targe.source, reFlags.exec(targe));
  result.lastIndex = targe.lastIndex;
  return result;
};

// 克隆symbol
const cloneSymbol = (targe) => {
  return Object(Symbol.prototype.valueOf.call(targe));
};
// 克隆函数
const cloneFunction = (func) => {
    const bodyReg = /(?<={)(.|\n)+(?=})/m;
    const paramReg = /(?<=().+(?=)\s+{)/;
    const funcString = func.toString();
    if (func.prototype) {
        const param = paramReg.exec(funcString);
        const body = bodyReg.exec(funcString);
        if (body) {
            if (param) {
                const paramArr = param[0].split(',');
                return new Function(...paramArr, body[0]);
            } else {
                return new Function(body[0]);
            }
        } else {
            return null;
        }
    } else {
        return eval(funcString);
    }
}
// 判断是否为引用数据类型,并且判断是否为 function 和 null
const isObject = (obj) => {
  const type = typeof obj;
  return obj !== null && (type === 'object' || type === 'function')
}
// 获取类型
const getType = (obj) => {
  return Object.peototype.toString.call(obj);
}
// 获取初始值
const getInit = (obj) => {
  const Con = obj.contructor;
  return new Con();
}
const clone = (obj, map = new WeakMap()) => {
  if (!isObject(obj)) {
    return obj;
  }
  const type = getType(obj);
  let new_obj;
  if (deepTag.includes(type)) {
         new_obj = getInit(obj);
  } else {
  	return cloneOtherType(target, type);
  }
  if (map.has(obj)) {
  	return map.get(obj);
  }
  map.set(obj, new_obj);
  // 克隆 set
  if (type === "[object Set]") {
  	obj.forEach((value) => {
    	new_obj.add(clone(value, map));
    })
  }
  // 克隆 map
  if (type === "[object Map]") {
  	obj.forEach((value, key) => {
    	new_obj.set(key, clone(value, map))
    })
  }
  // 克隆对象和数组
  if (type === "[object Object]" || type === "[object Array]") {
  	for (const key in obj) {
    	if (obj.hasOwnProperty(key)) {
      	new_obj[key] = clone(obj[key], map);
      }
    }
  }
  return new_obj;
}

广度优先遍历(将递归变为循环)

第一版

由于递归在数量过多的时候,就会出现爆栈的问题,所以可以用广度优先遍历来解决递归爆栈

const clone = (obj) => {
  let result = [];
  let stack = [{
  	parent: result,
    key: undefined,
    data: obj
  }]
  while (stack.length) {
    let {parent, key, data} = stack.pop();
    let res = parent;
    if (typeof key !== 'undefined') {
    	res = parent[key] = {};
    }
    for (const key in obj) {
    	if (data.hasOwnProperty(key)) {
      	if (typeof data[key] === 'object') {
        	strack.push([
          	parent: res,
            key,
            data: data[key]
          ])
        } else {
        	res[key] = data[key];
        }
      }
    }
  }
  return result;
}

第二版

解决循环引用

const getType = (obj) => {
  return Object.prototype.toString.call(obj);
}
const clone = (obj, map = new WeakMap()) {
  let result;
  if (getType(obj) === '[object Object]') {
  	result = {};
  } else if (getType(obj) === '[object Array]'){
  	result = [];
  }
  let stack = [{
    parent: result,
    key: undefined,
    data: obj
  }]
  while (stack.length) {
    let {parent, key, data} = stack.pop();
    let res = parent;
    if (typeof key !== 'undefined') {
    	res = parent[key] = getType(data) === '[object Object]' ? {} : [];
    }
    // 解决循环引用
    if (map.has(data)) {
      parent[key] = map.get(data);
      continue;
    }
    map.set(data, res);
    for (const key in data) {
    	if (data.hasOwnProperty(key)) {
      	if (getType(data) === '[object Object]' || getType(data) === '[object Array]') {
            stack.push({
            parent: res,
            key,
            data: data[key]
          })
        } else {
            res[key] = data[key];
        }
      }
    }
  }
  return result;
}

继承

借用构造函数实现继承

缺点:原型链上的方法无法继承

function Parent() {}
function Son() {
  Parent.call(this)
}

原型链实现继承

缺点:每个实例上的原型方法共享

function Parent() {}
function Son() {}
Son.prototype = new Parent();

组合式继承

缺点:会创建两次父类的实例

function Parent() {}
function Son() {
  Parent.call(this);
}
Son.prototype = new Parent();

组合式继承优化

缺点:子类的构造函数指向父类

function Parent() {}
function Son() {
  Parent.call(this);
}
Son.prototype = Parent.prototype;

组合式继承再次优化

function Parent() {}
function Son() {
  Parent.call(this);
}
Son.prototype = Object.create(Parent.prototype)

原型式继承

缺点:引用类型继承会被共享

function object(o) {
  function F() {};
  F.prototype = o;
  return new F();
}

寄生式继承

创建一个仅用于封装继承过程的函数,该函数在内部以某种形式来做增强对象,最后返回对象。

缺点:跟借用构造函数模式一样,每次创建对象都会创建一遍方法。

function object(o) {
  function F() {};
  F.prototype = o;
  return new F();
}
function createAnother(o) {
  let clone = object(o);
  clone.sayHi = function () {
    console.log('hi');
  }
  return clone;
}

寄生组合式继承

function object(o) {
  function F() {};
  F.prototype = o;
  return new F();
}
function inheritPrototype(father, son) {
  let prototype = object(father.prototype);
  prototype.constructor = son;
  son.prototype = prototype;
}

函数柯里化

什么叫函数柯里化?其实就是将使用多个参数的函数转换成一系列使用一个参数的函数的技术

函数柯里化有两种不同的场景,一种为函数参数个数定长的函数,另外一种为函数参数个数不定长的函数。

函数参数个数定长的柯里化解决方案

newArgs 为每次 addCurry(1)(2)(3)(4) 括号内的参数,将其每次都和 args 进行拼接,当 args 的长度和 fn 传入参数的长度相等时,则将 args 作为参数传入 fn 中

function curry(fn) {
  let args = [];
  return function _c(...newArgs) {
      if (newArgs.length) {
      args = [...args, ...newArgs];
      if (args.length === fn.length) {
      	return fn(...args);
      } else {
      	return _c;
      }
    }
  }
}
function add (a, b, c, d) {
    return [
        ...arguments
    ].reduce((a, b) => a + b)
}
let addCurry = currying(add)
let total = addCurry(1)(2)(3)(4) // 同时支持addCurry(1)(2, 3)(4)该方式调用
console.log(total) // 10

函数参数个数不定长的柯里化解决方案

newArgs 为每次 addCurry(1)(2)(3)(4) 括号内的参数,将其每次都和 args 进行拼接,当 newArgs 的值为空时,则代表传入结束,将拼接好的 args 传入到 fn 中

function curry(fn) {
  let args = [];
  return function _c(...newArgs) {
      if (newArgs.length) {
    	args = [...args, ...newArgs];
      return _c;
    } else {
    	return fn(...args);
    }
  }
}
function add (...args) {
	return args.reduce((a, b) => a + b)
}
let addCurry = currying(add)
// 注意调用方式的变化
console.log(addCurry(1)(2)(3)(4, 5)())

实现 JSON.stringify

JSON.stringify([, replacer [, space]) 方法是将一个 JavaScript 值(对象或者数组)转换为一个 JSON 字符串。此处模拟实现,不考虑可选的第二个参数 replacer 和第三个参数 space,如果对这两个参数的作用还不了解,建议阅读 MDN 文档。

  1. 基本数据类型:
  • undefined 转换之后仍是 undefined(类型也是 undefined)
  • boolean 值转换之后是字符串 "false"/"true"
  • number 类型(除了 NaN 和 Infinity)转换之后是字符串类型的数值
  • symbol 转换之后是 undefined
  • null 转换之后是字符串 "null"
  • string 转换之后仍是string
  • NaN 和 Infinity 转换之后是字符串 "null"
  1. 函数类型:转换之后是 undefined
  2. 如果是对象类型(非函数)
  • 如果是一个数组:如果属性值中出现了 undefined、任意的函数以及 symbol,转换成字符串 "null" ;

  • 如果是 RegExp 对象:返回 {} (类型是 string);

  • 如果是 Date 对象,返回 Date 的 toJSON 字符串值;

  • 如果是普通对象;

  • 如果有 toJSON() 方法,那么序列化 toJSON() 的返回值。

  • 如果属性值中出现了 undefined、任意的函数以及 symbol 值,忽略。

  • 所有以 symbol 为属性键的属性都会被完全忽略掉。

  1. 对包含循环引用的对象(对象之间相互引用,形成无限循环)执行此方法,会抛出错误。
function jsonStringify(data) {
  let dataType = typeof data;
  if (dataType !== 'object') {
    // 基本数据类型
    let result = data;
    if (Number.isNaN(data) || data === Infinity) {
    	result = 'null';
    } else if (dataType === 'function' || dataType === 'undefined' || dataType === 'symbol') {
    	result = undefined;
    } else if (dataType = 'string') {
    	result = '"' + data + '"';
    }
    return String(result);
  } else if (dataType === 'object') {
    // 引用数据类型,含有toJSON,数组,正则,null
    if (data === null) {
    	return 'null';
    } else if (data.toJSON && typeof data.toJSON === 'function') {
    	return jsonStringify(data.toJSON());
    } else if (Array.isArray(data)) {
      let result = [];
      data.forEach((item, index) => {
      	if (typeof item === 'undefined' || typeof item === 'function' || typeof item === 'symbol') {
        	result[index] = 'null';
        } else {
        	result[index] = jsonStringify(item);
        }
      })
      result = "[" + result + "]";
      return result.replace(/'/g, '"');
    } else {
      // 普通对象
      let result = [];
      Object.keys(data).forEach((key, index) => {
        if (typeof key !== 'symbol') {
          if (data[key] !== undefined && typeof data[key] !== 'function' && typeof data[key] !== 'symbol') {
            result.push('"' + key + '"' + ':' + jsonStringify(data[key]));
          }
        }
      })
      return ("{" + result + "}").replace(/'/g, '"');
    }
  }
}

Object.is

Object.is 解决的主要两个问题:

  1. +0 === -0 // true
  2. NaN === NaN // false

但是:

Object.is(+0, -0) // false

Object.is(NaN, NaN) // true

const is = (x, y) => {
  if (x === y) {
  	return x !== 0 || y !== 0 || 1/x === 1/y;
  } else {
  	return x !== x && y !== y;
  }
}

解析 URL 参数为对象

  1. 将 ? 后面的字符串取出来
  2. 将字符串以 & 分割后存到数组中
  3. 新建一个空对象 paramsObj 用于最后的输出
  4. 将 params 存到对象中
  5. 处理有 value 的值
  6. 分割 key 和 value
  7. 解码
  8. 判断是否转为数字
  9. 如果对象有 key,则添加一个值
  10. 如果对象没有这个 key,创建 key 并设置值
  11. 处理没有 value 的参数
function parseParam(url) {
  const paramsStr = /.+?(.+)$/.exec(url)[1];
  const paramsArr = paramsStr.splice('&');
  let paramsObj = {};
  paramsArr.forEach(param => {
      if (/=/.test(param)) {
    	let [key, val] = param.splice('=');
      val = decodeURIComponent(val);
      val = /^\d+$/.test(val) ? parseFloat(val) : val;
      if (paramsObj.hasOwnProperty(key)) {
      	paramsObj[key] = [].concat(paramsObj[key], val);
      } else {
      	paramsObj[key] = true;
      }
    }
  })
  return paramsObj;
}

Promise 相关

实现 Promise

Promise 状态

promise 有三个状态,pending、fulfilled、rejected

在 pending 状态,promise 可以切换到 fulfilled 或 rejected

在 fulfilled 状态,不能迁移到其他状态,必须有个不可变的 value

在 rejected 状态,不能迁移到其他状态,必须有个不变的 reason

const PENDING = 'pending';
const FUlFILLED = 'fulfilled';
const REJECTED = 'rejected';
function Promise() {
  this.state = PENDING;
  this.result = null;
}
const transition = (promise, state, result) => {
if (promise.state !== PENDING) return;
  promise.state = state;
  promise.result = result;
}

Then 方法

promise 必须接收一个 then 方法,接收 onFulfilled 和 onRejected 参数

onFulfilled 和 onRejected 如果是函数,必须最多执行一次

onFulfilled 的参数是 value,onRejected 函数的参数是 reason

then 方法可以被调用多次,每次注册一组 onFulfilled 和 onRejected 的 callback,他们如果被调用,必须按照注册顺序调用,必须返回 promise

  1. 在 then 方法里,return new Promise(f),满足 then 必须 return promise 的要求。
  2. 当 state 处于 pending 状态,就储存进 callbacks 列表里。
  3. 当 state 不是 pending 状态,就扔给 handleCallback 去处理。

至于为啥要套个 setTimeout 呢?

因为 then 方法里,还有一个重要约束是:

onFulfilled or onRejected must not be called until the execution context stack contains only platform code.

我们不是在 JS 引擎层面实现 Promises,而是使用 JS 去实现 JS Promises。在 JS 里无法主动控制自身 execution context stack。可以通过 setTimeout/nextTick 等 API 间接实现,此处选用了 setTimeout。

function Promise() {
  this.state = PENDING;
  this.result = null;
  this.callbacks = [];
}
promise.prototype.then = function (onFulfilled, onRejected) {
 return new Promise((resolve, reject) => {
  	let callback = {onFulfilled, onRejected, resolve, reject};
    if (this.state === PENDING) {
    	this.callbacks.push(callback);
    } else {
    	setTimeout(() => handleCallback(callback, this.state, this.result), 0);
    }
  })
}

handleCallback 方法

then 方法返回的 promise,也有自己的 state 和 result。它们将由 onFulfilled 和 onRejected 的行为指定。

  1. handleCallback 函数,根据 state 状态,判断是走 fulfilled 路径,还是 rejected 路径。
  2. 先判断 onFulfilled/onRejected 是否是函数,如果是,以它们的返回值,作为下一个 promise 的 result。
  3. 如果不是,直接以当前 promise 的 result 作为下一个 promise 的 result。
  4. 如果 onFulfilled/onRejected 执行过程中抛错,那这个错误,作为下一个 promise 的 rejected reason 来用。

then 方法核心用途是,构造下一个 promise 的 result。

const handleCallback = (callback. state, result) {
	let {onFulfilled, onRejected, resolve, reject} = callback;
  try {
  	if (state === FULFILLED) {
    	isFunction(onFulfilled) ? resolve(onFulfilled(result)) : resolve(result);
    } else if (state === REJECTED) {
    	isFunction(onRejected) ? reject(onRejected(result)) : reject(result);
    }
  } catch (error) {
  	reject(error);
  }
}

The Promise Resolution Procedure

一些特殊的 value 被 resolve 时,要做特殊的处理

  1. 如果 result 是当前 promise 本身,就抛出 TypeError 错误
  2. 如果 result 是另一个 promise,那么沿用它的 state 和 result 状态
  3. 如果 result 是一个 thenable 对象,先取 then 函数,再 call then 函数,重新进入 The Promise Resolution Procedure 过程。
  4. 如果不是上述情况,这个 result 成为当前 promise 的 result
const resolvePromise = (promise, result, resolve, reject) => {
  if (result === promise) {
    let reason = new TypeError('Can not fufill promise with itself');
    return reject(reason);
  }
  if (isPromise(result)) {
  	return result.then(resolve, reject);
  }
  if (isThenable(result)) {
    try {
    	let then = result.then;
      if (isFunction(then)) {
      	return new Promise(then.bind(result)).then(resolve, reject);
      }
    } catch (error) {
    	return reject(error);
    }
  }
} 

整合剩余部分

  1. 构造 onFulfilled 去切换到 fulfilled 状态,构造 onRejected 去切换到 rejected 状态
  2. 构造 resolve 和 reject 函数,在 resolve 函数里,通过 resolvePromise 对 value 进行验证
  3. 配合 ignore 这个 flag,保证 resolve/reject 只有一次调用作用
  4. 最后将 resolve/reject 作为参数,传入 f 函数
  5. 若 f 函数执行报错,该错误作为 reject 的 reason 来用
function Promise(f) {
  this.result = null;
  this.state = PENDING;
  this.callbacks = [];
  let onFulfilled = value => transition(this, FULFILLED, value);
  let onRejected = reason => transition(this, REJECTED, reason);
  let ignore = false;
  let resolve = value => {
  if (ignore) return;
    ignore = true;
    resolvePromise(this, value, onFulfilled, onRejected);
  }
  let reject = reason => {
  if (ignore) return;
    ignore = true;
    onRejected(reason);
  }
  try {
    f(resolve, reject);
  } catch (error) {
    reject(error);
  }
}

transition 函数扩充如上,当状态变更时,异步清空所有 callbacks。

const handleCallbacks = (callbacks, state, result) => {
  while (callbacks.length) handleCallback(callbacks.shift(), state, result);
}
const transition = (promise, state, result) => {
  if (promise.state !== PENDING) return;
  promise.state = state;
  promise.result = result;
  setTimeout(() => handleCallbacks(promise.callbacks, state, result), 0);
}

Promise.resolve

Promsie.resolve(value) 可以将任何值转成值为 value 状态是 fulfilled 的 Promise,但如果传入的值本身是 Promise 则会原样返回它。

Promise.resolve = function(value) {
  if (value instanceof Promise) {
      return value;
  }
  return new Promise(resolve => resolve(value));
}

Promise.reject

和 Promise.resolve() 类似,Promise.reject() 会实例化一个 rejected 状态的 Promise。但与 Promise.resolve() 不同的是,如果给 Promise.reject() 传递一个 Promise 对象,则这个对象会成为新 Promise 的值。

Promise.rejected = function(reason) {
    return new Promise((resolve, reject) => reject(reason))
}

Promise.all

Promise.all 的规则是这样的:

  • 传入的所有 Promsie 都是 fulfilled,则返回由他们的值组成的,状态为 fulfilled 的新 Promise;
  • 只要有一个 Promise 是 rejected,则返回 rejected 状态的新 Promsie,且它的值是第一个 rejected 的 Promise 的值;
  • 只要有一个 Promise 是 pending,则返回一个 pending 状态的新 Promise;
Promise.all = function(promiseArr) {
  let count = 0, result = [];
  return new Promise((resolve, reject) => {
    if (!Array.isArray(promiseArr)) {
    	return reject(new Error('传入的参数不是数组'));
    }
  	promiseArr.forEach((p, i) => {
    	Promise.resolve(p).then(val => {
      	count++;
        result[i] = val;
        if (count === promiseArr.length) {
        	resolve(result);
        }
      }).catch(e = reject(e));
    })
  })
}

Promise.race

Promise.race 会返回一个由所有可迭代实例中第一个 fulfilled 或 rejected 的实例包装后的新实例。

Promise.race = function(promiseArr) {
    return new Promise((resolve, reject) => {
  	if (!Array.isArray(promiseArr)) {
    	return new Error('请输入数组');
    }
    promiseArr.forEach(p => {
    	Promise.resolve(p).then(val => {
      	resolve(val)
      }).catch(e => reject(e))
    })
  })
}

Promise.allSettled

Promise.allSettled 的规则是这样:

  • 所有 Promise 的状态都变化了,那么新返回一个状态是 fulfilled 的 Promise,且它的值是一个数组,数组的每项由所有 Promise 的值和状态组成的对象;
  • 如果有一个是 pending 的 Promise,则返回一个状态是 pending 的新实例;
Promise.allSettled = function(promiseArr) {
  let result = [];
  return new Promise((resolve, reject) => {
    if (!Array.isArray(promiseArr)) {
    	return reject(new Error('请输入数组'));
    }
    promiseArr.forEach((p, i) => {
    	Promise.resolve(p).then(val => {
      	result[i] = {
        	status: 'fulfilled',
          value: val
        }
        if (result.length === promiseArr.length) {
        	resolve(result);
        }
      })
      .catch(error => {
      	result[i] = {
        	status: 'rejected',
          reason: error
        }
        if (result.length === promiseArr.length) {
        	resolve(result);
        }
      })
    })
  })
}

Promise.any

Promise.any 的规则是这样:

  • 空数组或者所有 Promise 都是 rejected,则返回状态是 rejected 的新 Promsie,且值为 AggregateError 的错误;
  • 只要有一个是 fulfilled 状态的,则返回第一个是 fulfilled 的新实例;
  • 其他情况都会返回一个 pending 的新实例
Promise.any =function(promiseArr) {
  let count = 0;
  let result = [];
  return new Promise((resolve, reject) => {
    if (!promiseArr.length) return;
    promise.forEach((p, i) => {
    	Promise.resolve(p).then(val => {
      	resolve(val);
      })
    }).catch(error => {
      result[count] = error;
      count++;
      if (count === promiseArr.length) {
      	reject(result);
      }
    })
  })
}

Promise.prototype.finally

它就是一个语法糖,在当前 promise 实例执行完 then 或者 catch 后,均会触发。

Promise.prototype.finally 的执行与 promise 实例的状态无关,不依赖于 promise 的执行后返回的结果值。其传入的参数是函数对象。

实现思路:

  • 考虑到 promise 的 resolver 可能是个异步函数,因此 finally 实现中,要通过调用实例上的 then 方法,添加 callback 逻辑
  • 成功透传 value,失败透传 error
Promise.prototype.finally = function(cb) {
    return this.then(
  	value => Promise.resolve(cb()).then(() => value),
    error => {
    	Promise.resolve(cb()).then(() => {
      	throw error;
      })
    }
  )
}

参考文献

  1. 一个递归爆栈引起的对深拷贝的理解
  2. 【重学JS之路】继承
  3. 【重学JS之路】new关键词
  4. 【重学JS之路】深拷贝和浅拷贝
  5. 100 行代码实现 Promises/A+ 规范
  6. 死磕 36 个 JS 手写题(搞懂后,提升真的大)

后语

觉得还可以的,麻烦走的时候能给点个赞,大家一起学习和探讨!