# 手写题

# 实现一个闭包并分析一段代码讲出原因

实现一个简单闭包

function sum(a) {
  return function(b) {
    return a + b;
  };
}

var result = sum(1)(2);
console.log(result); // 3

分析代码

for (let i = 1; i < 5; i++) {
  setTimeout(function timer() {
    console.log(i);
  }, i * 1000);
}
//输出 1,2,3,4
//let 绑定 for 循环,将其重新绑定到每一次的迭代中,保证每次迭代结束都会重新赋值
//有自己的作用域块,
//var 没有自己的作用域块,所以循环变量就会后一个覆盖前一个,循环完毕只有一个值输出;
for (var i = 0; i < 5; i++) {
  setTimeout(() => {
    console.log(i);
  }, i * 1000);
}
// 输出 5,5,5,5

# 手写 JSONP

JSONP 函数一共需要三个参数: url,params,callback

  1. 声明一个挂在全局的函数,函数名为 callback,获取服务器的返回的 data

  2. callbackparams作为一个对象拼接参数

  3. 新建 script 标签,将 src 设置为拼接好的参数,然后挂在到 body

function JSONP({ url, params, callback }) {
  return new Promise((resolve, reject) => {
    let script = document.createElement("script");
    // 声明回调
    window[callback] = function(data) {
      resolve(data);
      document.body.removeChild(script);
    };
    // 拼接参数
    params = { ...params, callback };
    let arr = [];
    for (let key in params) {
      arr.push(`${key}=${params[key]}`);
    }
    // 赋值src
    script.src = `${url}?${arr.join("&")}`;
    document.body.append(script);
  });
}

// 使用实例

jsonp({
  url: "http://localhost:3000/say",
  params: { wd: "Iloveyou" },
  callback: "show"
}).then(data => {
  console.log(data);
});

# 手写 repeat 函数

function repeat(func, times, wait) {
  // TODO
}
const repeatFunc = repeat(alert, 4, 3000);
// 调用这个 repeatFunc ("hellworld"),会alert4次 helloworld, 每次间隔3秒
async function sleep(fn, wait, args) {
  return new Promise(resolve => {
    setTimeout(() => {
      fn.apply(this, args);
      resolve();
    }, wait);
  });
}
function repeat(func, times, wait) {
  return async function() {
    for (let i = 0; i < times; i++) {
      await sleep(func, wait, arguments);
    }
  };
}
var repeatFunc = repeat(alert, 4, 3000);
repeatFunc("helloworld");

# 解析 URL

img

protocol://username:passwrod + @ + hostname:port + pathname + search + hash

协议 + 用户名 + 用户名: 密码 + 域名 + 端口 +路径 + 参数 +哈希 https://keith:miao@www.foo.com:80/file?test=3&miao=4#heading-0

https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Basicsof_HTTP/Identifying_resources_on_the_Web#%E7%BB%9F%E4%B8%80%E8%B5%84%E6%BA%90%E6%A0%87%E8%AF%86%E7%AC%A6%E7%9A%84%E8%AF%AD%E6%B3%95(uri) (opens new window)

function parseUrl(url) {
  const urlObj = new URL(url);
  return {
    protocol: urlObj.protocol,
    username: urlObj.username,
    password: urlObj.password,
    hostname: urlObj.hostname,
    port: urlObj.port,
    pathname: urlObj.pathname,
    search: urlObj.search,
    hash: urlObj.hash
  };
}

# 解析 URL 参数

//拆分字符串形式
function queryToObj() {
  const res = {};
  const search = location.search.substr(1); //去掉前面的“?”
  search.split("&").forEach(paramStr => {
    const arr = paramStr.split("=");
    const key = arr[0];
    const val = arr[1];
    res[key] = val;
  });
  return res;
}

//正则形式
function queryRegExp(name) {
  const search = location.search.substr(1);
  const reg = new RegExp(`(^|&)${name}=([^&]*)(&|$)`, "i");
  const res = search.match(reg);
  if (res == null) {
    return null;
  }
  return res[2];
}

// URLSearchParams
function queryParams() {
  let search = location.search.slice(1);
  let p = new URLSearchParams(search);
  /**
   * get(key) :获取参数值
   * getAll(key): 获取所有的参数
   * has(key) 检验是否有
   * keys和values返回两个迭代器,可以使用for of 遍历拿到值
   * */
  // 解析为对象
  return Object.fromEntries(p.entries());
}

# 实现驼峰转换和反转

/**
 * 输入:
content-type
输出:
contentType
 * */

function camel(str) {
  // TODO
  let ans = "";
  let upper = false;
  for (let index = 0; index < str.length; index++) {
    const element = str[index];
    if (element == "_" || element == "-" || element == "@") {
      upper = true;
    } else {
      if (upper) {
        ans += element.toUpperCase();
      } else {
        ans += element;
      }
      upper = false;
    }
  }
  return ans;
}

// 正则
var f = function(s) {
  return s.replace(/-\w/g, function(x) {
    return x.slice(1).toUpperCase();
  });
};

# 求数组(字符串)的交集和并集合差集

var s1 = "acacger";
var s2 = "abcgac";

var a1 = [1, 2, 3, 4, 5, 6, 6];
var a2 = [2, 3, 4];

const a = [...new Set(a1)];
const b = [...new Set(a2)];
// 交集
console.log(a.filter(item => b.includes(item)));

// 补集
console.log([...new Set([...a, ...b])]);

// 差集
console.log(a.filter(item => !b.includes(item)));

# 深层判断对象是否相同

/*
 * @param x {Object} 对象1
 * @param y {Object} 对象2
 * @return  {Boolean} true 为相等,false 为不等
 */
export const deepEqual = (x, y) => {
  // 指向同一内存时
  if (x === y) {
    return true;
  } else if (typeof x == "object" && x != null && typeof y == "object" && y != null) {
    if (Object.keys(x).length !== Object.keys(y).length) {
      return false;
    }
    for (var prop in x) {
      if (y.hasOwnProperty(prop)) {
        if (!deepEqual(x[prop], y[prop])) return false;
      } else {
        return false;
      }
    }
    return true;
  } else {
    return false;
  }
};

# 数组去重

function unique(arr) {
  return [...new Set(arr)];
}

function unique(arr) {
  // 当前值 下标 数组
  return arr.filter((cur, index, array) => {
    return array.indexOf(cur) === index;
  });
}

function unique(arr) {
  return arr.reduce((pre, cur) => {
    if (!pre.includes(cur)) {
      return pre.concat(cur);
    } else {
      return pre;
    }
  }, []);
}

function arrayNonRepeatfy(arr) {
  let map = new Map();
  let array = new Array(); // 数组用于返回结果
  for (let i = 0; i < array.length; i++) {
    if (map.has(array[i])) {
      // 如果有该key值
      map.set(array[i], true);
    } else {
      map.set(array[i], false); // 如果没有该key值
      array.push(array[i]);
    }
  }
  return array;
}

# 浅拷贝

只是复制栈的内存地址,如果另一个对象改变堆的值,本身会发生改变

  1. 引用复制
function shallowCopy(target) {
  const res = {};
  for (let key in target) {
    res[key] = target[key];
  }
  return res;
}

//
function shallowCopy(object) {
  // 只拷贝对象
  if (!object || typeof object !== "object") return;

  // 根据 object 的类型判断是新建一个数组还是对象
  let newObject = Array.isArray(object) ? [] : {};

  // 遍历 object,并且判断是 object 的属性才拷贝
  for (let key in object) {
    if (object.hasOwnProperty(key)) {
      newObject[key] = object[key];
    }
  }

  return newObject;
}
  1. Object.assign
var x = {
  a: 1,
  b: { f: { g: 1 } },
  c: [1, 2, 3]
};
var y = Object.assign({}, x);
console.log(y.b.f === x.b.f); // true
  1. 利用扩展运算符

# 深拷贝

完全拷贝,互不影响

  1. 递归实现
const deepClone = (target = {}, map = new WeakMap()) => {
  // 基本数据
  if (typeof target !== "object") return target;
  // Date
  if (target instanceof Date) return new Date(target);
  // RegExp source - 文本,flags - 标志
  // https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/RegExp
  if (target instanceof RegExp) return new RegExp(target.source, target.flags);
  // function
  if (target instanceof Function) {
    return function() {
      return target.apply(this, [...arguments]);
    };
  }

  // 避免循环引用
  if (map.has(target)) return target;
  map.set(target, true);

  let res = Array.isArray(target) ? [] : {};

  for (let key in target) {
    if (target.hasOwnProperty(key)) {
      res[key] = deepClone(target[key]);
    }
  }

  return res;
};

# JS 继承方式

# 原型链继承

此类继承方式虽然简单,但是会继承很多多余的属性

Grand.prototype.lastName = "deng";
function Grand() {}
var grand = new Grand();
Father.prototype = grand;
function Father() {
  this.name = "hiehie";
}
var father = new Father();
Son.prototype = father;
function Son() {}
var son = new Son();

# 构造函数继承

这种调用父级构造函数逇方式其实和上面那个一样,虽然写法好看了,但是无法借用构造函数的原型

function Person(name, age) {
  this.age = age;
  this.name = name;
}
function Student(name, age, grad) {
  Person.call(this, name, age);
  this.grad = grad;
}

var student = new Student("1", 2, 2);

# 公有原型继承

这种方式缺点是无法新增自己的原型属性,会互相影响

Father.prototype.lastName = "李雷";
function Father() {}
function Son() {}
// 两个构造函数继承同一个原型
Son.prototype = Father.prototype;
var son = new Son();
var father = new Father();
Son.prototype.name = "lihua"; //这样会导致Father.prototype = "lihua"

# 圣杯模式继承

使用了一个中间函数做过渡,弥补前两种方式的缺点,解决了公有原型无法增加私有原型属性的缺点

function inherit(Target, Origin) {
  function F() {}
  F.prototype = Origin.prototype;
  // 通过原型链继承  只不过经过了中间层F
  Target.prototype = new F();
  // son.__proto ===> newF().__proto__===>Father.prototype
  // 这一步是因为原型链将son的__proto指向了Father的prototype 所以要指回去
  Target.prototype.constuctor = Target;
}

# 寄生组合继承

function inheritPrototype(subType, superType) {
  var prototype = Object.create(superType.prototype); // 创建对象,创建父类原型的一个副本
  prototype.constructor = subType; // 增强对象,弥补因重写原型而失去的默认的constructor 属性
  subType.prototype = prototype; // 指定对象,将新创建的对象赋值给子类的原型
}
  1. 只调用一次父类的构造函数,避免了在子类原型上创建不必要的,多余的属性
  2. 原型链保持不变

# 对象数组去重

const responseList = [
  { id: 1, a: 1 },
  { id: 2, a: 2 },
  { id: 3, a: 3 },
  { id: 1, a: 4 }
];
const result = responseList.reduce((acc, cur) => {
  const ids = acc.map(item => item.id);
  return ids.includes(cur.id) ? acc : [...acc, cur];
}, []);
console.log(result); // -> [ { id: 1, a: 1}, {id: 2, a: 2}, {id: 3, a: 3} ]

# 手写数组扁平化

function flatten(arr) {
  return [].concat(
    ...arr.map(v => {
      return Array.isArray(v) ? flatten(v) : v;
    })
  );
}

function flatten(arr) {
  return arr.reduce((pre, cur) => {
    return pre.concat(Array.isArray(cur) ? flatten(cur) : cur);
  }, []);
}

function flat(arr, num = 1) {
  return num > 0
    ? arr.reduce((prev, cur) => {
        return prev.concat(Array.isArray(cur) ? flat(arr, num - 1) : cur);
      }, [])
    : arr.slice();
}

Array.prototype.flat5 = function() {
  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();
};

# 手写对象扁平化

function objectFlat(obj = {}) {
  const res = {};
  function flat(item, preKey = "") {
    Object.entries(item).forEach(([key, val]) => {
      // 如果有前一个key,则拼接当前的newKey
      const newKey = preKey ? `${preKey}.${key}` : key;
      if (val && typeof val === "object") {
        // 递归
        flat(val, newKey);
      } else {
        res[newKey] = val;
      }
    });
  }
  flat(obj);
  return res;
}

// 测试
const source = { a: { b: { c: 1, d: 2 }, e: 3 }, f: { g: 2 } };
console.log(objectFlat(source));

# 实现防抖函数(debounce)

核心思想: 每次事件触发则删除原来的定时器,建立新的定时器。跟王者荣耀的回城功能类似,你反复触发回城功能,那么只认最后一次,从最后一次触发开始计时。

function debounce(fn, delay) {
  let timer = null;
  return function(...args) {
    if (timer) clearTimeout(timer);
    timer = setTimeout(function() {
      fn.apply(this, args);
    }, delay);
  };
}

适用场景:

  • 按钮提交场景:防止多次提交按钮,只执行最后提交的一次
  • 服务端验证场景:表单验证需要服务端配合,只执行一段连续的输入事件的最后一次,还有搜索联想词功能类似

# 实现节流函数(throttle)

节流的核心思想: 如果在定时器的时间范围内再次触发,则不予理睬,等当前定时器完成,才能启动下一个定时器任务。这就好比公交车,10 分钟一趟,10 分钟内有多少人在公交站等我不管,10 分钟一到我就要发车走人!

function throttle(fn, interval) {
  let flag = true;
  return function(...args) {
    if (!flag) return;
    flag = false;
    setTimeout(() => {
      fn.apply(this, args);
      flag = true;
    }, interval);
  };
}

适用场景:

  • 拖拽场景:固定时间内只执行一次,防止超高频次触发位置变动

  • 缩放场景:监控浏览器 resize

  • 动画场景:避免短时间内多次触发动画引起性能问题

# 洗牌算法和获取随机数

function getRandomInt(min, max) {
  // 取min-max的数据
  return Math.floor(Math.random() * (max - min + 1) + min);
}

export function shuffle(arr) {
  // 当前数组值与取到的随机数组值交换 从而打乱数据
  let _arr = arr.slice();
  for (let i = 0; i < _arr.length; i++) {
    let j = getRandomInt(0, i);
    let t = _arr[i];
    _arr[i] = _arr[j];
    _arr[j] = t;
  }
  return _arr;
}

# 实现完整版 typeof

function getType(obj) {
  if (obj === null) return String(obj);
  return typeof obj === "object"
    ? Object.prototype.toString
        .call(obj)
        .replace("[object ", "")
        .replace("]", "")
        .toLowerCase()
    : typeof obj;
}

// 调用
getType(null); // -> null
getType(undefined); // -> undefined
getType({}); // -> object
getType([]); // -> array
getType(123); // -> number
getType(true); // -> boolean
getType("123"); // -> string
getType(/123/); // -> regexp
getType(new Date()); // -> date

# 实现 instanceof

function instanceof(L, R) {
  let O = R.prototype;
  L = L.__proto__;
  while (true) {
    if (L === null) {
      return false;
    }
    if (L === O) {
      return true;
    }
    L = L.__proto__;
  }
}

# 模拟 new

function MyNew(ctor, ...args) {
  const obj = Object.create(ctor.prototype);
  const res = ctor.apply(obj, args);
  return res instanceof Object ? res : obj;
}

#

# Object.create

const myCreate = function(obj) {
  function F() {}
  F.prototype = obj;
  return new F(); // 创建一个继承 obj 原型属性的纯净对象
};

#

# call、apply、bind

var foo = {
  value: 1
};

function bar() {
  console.log(this.value);
}

bar.call(foo); // 1

// 想要将bar的this指向foo,就应该将bar作为foo对象的属性,用foo去调用就可以完成赋值
// 以下context可以视为 foo,context[caller]视为bar
var foo = {
  value: 1,
  bar: function() {
    console.log(this.value);
  }
};

foo.bar(); // 1
// call
Function.prototype.call_ = function MyCall(context, ...args) {
  context = context || window;
  const caller = new Simbol("caller");
  context[caller] = this;
  const res = context[caller](...args);
  delete context.caller;
  return res;
};
//apply
Function.prototype.apply_ = function MyApply(context, args) {
  context = context || window;
  const caller = new Simbol("caller");
  context[caller] = this;
  const res = context[caller](...args);
  delete context.caller;
  return res;
};
//bind
Function.prototype.bind_ = function() {
  // 转为数组
  const args = Array.prototype.slice.call(arguments);
  // 取出this同时取出调用参数
  let t = args.shift();
  // 返回的是一个函数
  return function() {
    this.apply(t, args);
  };
};

# 手写一个 count 函数

每次调用一个函数自动加 1

count() 1
count() 2
count() 3

var count = (function() {
  var a = 0;
  return function() {
    console.log(++a);
  };
})();

count(); // 1
count(); // 2
count(); // 3

# 手写一个 sleep 睡眠函数

比如 sleep(1000)代表等待 1000ms

方法一:ES5 方式实现

function sleep(callback, time) {
  if (typeof callback == "function") {
    setTimeout(callback, time);
  }
}
function output() {
  console.log(111);
}
sleep(output, 2000);

方法二:使用 promise 方式

const sleep = time => {
  return new Promise(resolve => {
    setTimeout(resolve, time);
  });
};
sleep(2000).then(() => {
  console.log(111);
});

# 手写 forEach

forEach()方法对数组的每个元素执行一次给定的函数。

arr.forEach(function(currentValue, currentIndex, arr) {}, thisArg);

//currentValue  必需。当前元素
//currentIndex  可选。当前元素的索引
//arr           可选。当前元素所属的数组对象。
//thisArg       可选参数。当执行回调函数时,用作 this 的值。
Array.prototype._forEach = function(fn, thisArg) {
  let arr = Array.prototype.slice.call(this);
  for (let i = 0; i < arr.length; i++) {
    fn.call(thisArg, arr[i], i, arr);
  }
};

// test
let arr = [1, 2, 3, 4, 5];
arr._forEach((item, index) => {
  console.log(item, index);
});

// test thisArg

# 手写 filter 方法

每次需要将符合条件的元素放到返回数组中去

Array.prototype._fillter = function(fn, thisArg) {
  let arr = Array.prototype.slice.call(this);
  let resArr = [];
  for (let i = 0; i < arr.length; i++) {
    fn.call(thisArg, arr[i], i, arr) && resArr.push(arr[i]);
  }
};

# 手写 reduce 方法

Array.prototype.myReduce = function(fn, initialValue) {
  var arr = Array.prototype.slice.call(this);
  var res, startIndex;
  res = initialValue ? initialValue : arr[0]; // 不传默认取数组第一项
  startIndex = initialValue ? 0 : 1;
  for (var i = startIndex; i < arr.length; i++) {
    // 把初始值、当前值、索引、当前数组返回去。调用的时候传到函数参数中 [1,2,3,4].reduce((initVal,curr,index,arr))
    res = fn.call(null, res, arr[i], i, this);
  }
  return res;
};

# 用 reduce 实现 map

reduce 是一个累加方法,是对数组累积执行回调函数,返回最终计算结果。

array.reduce(function(prev, currentValue, currentIndex, arr) {}, initialValue);

//pre 上一次回调返回值。
//currentValue  必需。当前元素
//currentIndex  可选。当前元素的索引
//arr   可选。当前元素所属的数组对象。
//initialValue可选。传递给函数的初始值

map 是遍历数组的每一项,并执行回调函数的操作,返回一个对每一项进行操作后的新数组。

array.map(function(currentValue,index,arr), thisArg)//currentValue  必须。当前元素的值
//index 可选。当前元素的索引值
//arr   可选。当前元素属于的数组对象
//thisArg 可选。对象作为该执行回调时使用,传递给函数,用作 "this" 的值。如果省略了 thisArg,或者传入 null、undefined,那么回调函数的 this 为全局对象。

用 reduce 实现 map 方法

Array.prototype.myMap = function(fn, thisArg) {
  var res = [];
  thisArg = thisArg || [];
  this.reduce(function(pre, cur, index, arr) {
    res.push(fn.call(thisArg, cur, index, arr));
  }, []);
  return res;
};
var arr = [2, 3, 1, 5];
arr.myMap(function(item, index, arr) {
  console.log(item, index, arr);
});
let res = arr.myMap(v => v * 2);
console.log(res); // [4,6,2,10]

ES5 实现

Array.prototype.MyMap = function(fn, context) {
  // 转换类数组
  var arr = Array.prototype.slice.call(this); //由于是ES5所以就不用...展开符了
  var mappedArr = [];
  for (var i = 0; i < arr.length; i++) {
    // 把当前值、索引、当前数组返回去。调用的时候传到函数参数中 [1,2,3,4].map((curr,index,arr))
    mappedArr.push(fn.call(context, arr[i], i, arr));
  }
  return mappedArr;
};

# 手写 find 方法

Array.prototype.myFind = function(callback) {
  // var callback = function (item, index) { return item.id === 4 }
  for (var i = 0; i < this.length; i++) {
    if (callback(this[i], i)) {
      return this[i];
    }
  }
};

# 手写 findIndex 方法

Array.prototype.myFindIndex = function(callback) {
  // var callback = function (item, index) { return item.id === 4 }
  for (var i = 0; i < this.length; i++) {
    if (callback(this[i], i)) {
      // 这里返回
      return i;
    }
  }
};

# 实现 (5).add(3).minus(2) 功能

例: 5 + 3 - 2,结果为 6

Number.prototype.add = function(n) {
  return this.valueOf() + n;
};
Number.prototype.minus = function(n) {
  return this.valueOf() - n;
};

# ['1', '2', '3'].map(parseInt)返回值

首先返回值为: [1, NaN, NaN]

map 是传入的函数是有 3 个参数的: value, index, arr, 而 parseInt 有两个参数:

parseInt(string, radix);

string
要被解析的值。如果参数不是一个字符串,则将其转换为字符串(使用toString 抽象操作)。字符串开头的空白符将会被忽略。

radix 可选
从 2 到 36,表示字符串的基数。例如指定 16 表示被解析值是十六进制数。请注意,10不是默认值!

所以['1', '2', '3'].map(parseInt)的过程是这样子的:

parseInt("1", 0); // radix是0的情况见如下解释 ,当基数等于0时,radix被假定为10进制
parseInt("2", 1); // radix基数只能取到 2 - 36 之间,所以NaN
parseInt("3", 2); // radix=2 表示是二进制数,只能有0和1,解析的字符串是'3',所以是NaN

# 手写一个 promise

const STATUS_MAP = {
  PENDING: "pending",
  RESOLVED: "resolved",
  REJECTED: "rejected"
};
function myPromise1(constructor) {
  let self = this;

  self.status = STATUS_MAP.PENDING;
  self.value = undefined;
  self.reason = undefined;

  function resolve(value) {
    if (self.status === STATUS_MAP.PENDING) {
      self.value = value;
      self.status = STATUS_MAP.RESOLVED;
    }
  }

  function reject(reason) {
    if (self.status === STATUS_MAP.PENDING) {
      self.reason = reason;
      self.status = STATUS_MAP.REJECTED;
    }
  }

  // 捕获构造异常
  try {
    constructor(resolve, reject);
  } catch (error) {
    reject(error);
  }
}

myPromise1.prototype.then = function(onFullfilled, onRejected) {
  let self = this;

  switch (self.status) {
    case STATUS_MAP.RESOLVED:
      onFullfilled(self.value);
      break;
    case STATUS_MAP.REJECTED:
      onRejected(self.reason);
    default:
      break;
  }
};

myPromise1.race = function(iterator) {
  if (!Array.isArray(iterator)) {
    return;
  }

  return new Promise((resolve, reject) => {
    for (let i of iterator) {
      Promise.resolve(i)
        .then(data => resolve(data))
        .catch(e => {
          reject(e);
        });
    }
  });
};

myPromise1.all = function(iterator) {
  if (!Array.isArray(iterator)) {
    return;
  }

  let res = [];
  let count = 0;

  return new Promise((resolve, reject) => {
    for (let i of iterator) {
      Promise.resolve(i)
        .then(data => {
          res[count++] = data;

          if (count === iterator.length) {
            resolve(res);
          }
        })
        .catch(e => {
          reject(e);
        });
    }
  });
};

const test = new myPromise1((resolve, reject) => {
  resolve(1);
});
test.then(res => {
  console.log(res);
});

# 手写Promise的静态方法

# race

myPromise1.race = function(iterator) {
  if (!Array.isArray(iterator)) {
    return;
  }

  return new Promise((resolve, reject) => {
    for (let i of iterator) {
      Promise.resolve(i)
        .then(data => resolve(data))
        .catch(e => {
          reject(e);
        });
    }
  });
};

# all

myPromise1.all = function(iterator) {
  if (!Array.isArray(iterator)) {
    return;
  }

  let res = [];
  let count = 0;

  return new Promise((resolve, reject) => {
    for (let i of iterator) {
      Promise.resolve(i)
        .then(data => {
          res[count++] = data;

          if (count === iterator.length) {
            resolve(res);
          }
        })
        .catch(e => {
          reject(e);
        });
    }
  });
};

Promise.all = function(promises) {
  return new Promise((resolve, reject) => {
    let result = [];
    let index = 0;
    let len = promises.length;
    if (len === 0) {
      resolve(result);
      return;
    }

    for (let i = 0; i < len; i++) {
      // 为什么不直接 promise[i].then, 因为promise[i]可能不是一个promise
      Promise.resolve(promise[i])
        .then(data => {
          result[i] = data;
          index++;
          if (index === len) resolve(result);
        })
        .catch(err => {
          reject(err);
        });
    }
  });
};

# resolve

Promise.resolve = function(value) {
  return new Promise((resolve, reject) => {
    resolve(value);
  });
};

# reject

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

# Promisify

// Promisify
function Promisify(origin) {
  return function(...args) {
    return new Promise((resolve, reject) => {
      // args 尾部添加回调函数(参数为错误信息,values),用于改变promise状态
      args.push(function callback(error, ...values) {
        if (error) {
          return reject(error);
        }
        return resolve(...values);
      });

      // 执行原函数
      origin.call(this, ...args);
    });
  };
}

// callback 方式: stat(path, (err, res) => .....)
// promise 方式: promisify(stat)(path).then(res => console.log(res))

# Promise 实现网络超时判断

const uploadFile = (url, params) => {
  return Promise.race([uploadFilePromise(url, params), uploadFileTimeout(3000)]);
};
function uploadFilePromise(url, params) {
  return new Promise((resolve, reject) => {
    axios
      .post(url, params, {
        headers: { "Content-Type": "multipart/form-data" }, // 以formData形式上传文件
        withCredentials: true
      })
      .then(res => {
        if (res.status === 200 && res.data.code === 0) {
          resolve(res.data.result);
        } else {
          reject(res.data);
        }
      });
  });
}
function uploadFileTimeout(time) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      reject({ timeoutMsg: "上传超时" });
    }, time);
  });
}
function f(fn, time) {
  let timeFn = new Promise((_, reject) => {
    setTimeout(() => {
      reject("error");
    }, time);
  });

  return Promise.race(fn, timeFn);
}

# 基于 Promise.all 实现 Ajax 的串行和并行

# 串行

所谓串行也就是执行完一个操作之后再去执行另一个操作

const serial2 = tasks => {
  let index = 0;
  const resArr = [];
  return new Promise((resolve, reject) => {
    const doTask = () => {
      fecth(tasks[index])
        .then(res => {
          index++;
          resArr.push(res);
          if (index == tasks.length) {
            return resolve(resArr);
          }
          doTask();
        })
        .catch(err => {
          return reject(err);
        });
    };
    doTask();
  });
};

# 并行

使用 Promise.all() 实现并行操作

var promises = function() {
  return [1000, 2000, 3000].map(current => {
    return new Promise((resolve, reject) => {
      setTimeout(() => {
        console.log(current);
      }, current);
    });
  });
};

Promise.all(
  promises().then(() => {
    console.log("end");
  })
);
实现一个批量请求函数 multiRequest(urls, maxNum),要求如下:
• 要求最大并发数 maxNum
• 每当有一个请求返回,就留下一个空位,可以增加新的请求
• 所有请求完成后,结果按照 urls 里面的顺序依次打出

使用递归调用的方式进行实现

function mutilyRequest(urls, limit) {
  let length = urls.length;
  let result = Array(length).fill(false);
  let count = 0;
  return new Promise(resolve => {
    while (count < limit) {
      next();
    }
    function next() {
      let current = count++;
      if (current >= length) {
        !result.includes(false) && resolve(result);
        return;
      }

      let url = urls[current];
      console.log(`开始请求第${current}个接口,请求的url为${url}`);
      ajax(url)
        .then(data => {
          result[current] = data;
          console.log(`${current}个请求已完成`);
          if (current < length) {
            next();
          }
        })
        .catch(error => {
          result[current] = error;
          console.log(`${current}个请求发生错误`);
          if (current < length) {
            next();
          }
        });
    }
  });
}

function ajax(url) {
  return new Promise(resolve => {
    resolve(`当前请求${url}`);
  });
}

mutilyRequest(["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10"], 4).then(res => {
  console.log(res);
});

// 打印结果如下
// 开始请求第0个接口,请求的url为0
// 开始请求第1个接口,请求的url为1
// 开始请求第2个接口,请求的url为2
// 开始请求第3个接口,请求的url为3
// 第0个请求已完成
// 开始请求第4个接口,请求的url为4
// 第1个请求已完成
// 开始请求第5个接口,请求的url为5
// 第2个请求已完成
// 开始请求第6个接口,请求的url为6
// 第3个请求已完成
// 开始请求第7个接口,请求的url为7
// 第4个请求已完成
// 开始请求第8个接口,请求的url为8
// 第5个请求已完成
// 开始请求第9个接口,请求的url为9
// 第6个请求已完成
// 开始请求第10个接口,请求的url为10
// 第7个请求已完成
// 第8个请求已完成
// 第9个请求已完成
// 第10个请求已完成
// [
//   '当前请求0',  '当前请求1',
//   '当前请求2',  '当前请求3',
//   '当前请求4',  '当前请求5',
//   '当前请求6',  '当前请求7',
//   '当前请求8',  '当前请求9',
//   '当前请求10'
// ]

# 手写 Vue 双向数据绑定

# Vue2 Object.definPropetry()

  1. 对象,包含复杂对象

  2. 值类型

  3. 数组,改造原型

const oldArrayPrototype = Array.prototype;
// 将ArraayObject原型指向他,然后扩展方法 即继承数组方法,不影响定义新方法
const newArrayObject = Object.create(oldArrayPrototype)[
  // 扩展常用方法
  ("push", "shift", "unshift", "pop")
].forEach(methodName => {
  // 扩展的方法
  newArrayObject[methodName] = function() {
    console.log("视图更新");
    // 真正意义上的调用原生方法
    oldArrayPrototype[methodName].call(this, ...arguments);
  };
});

function observer(target) {
  if (typeof target !== object || target == null) {
    // 非对象或数组
    return target;
  }
  if (target instanceof Array) {
    target.__proto__ = newArrayObject;
  }
  for (let key in target) {
    defineReactive(target, key, target[key]);
  }
}

function defineReactive(data, key, value) {
  // value可能是复杂对象 递归监听
  observer(value);
  Object.definePropetry(data, key, {
    get: function() {
      return value;
    },
    set: function(newVal) {
      // 深度监听
      observer(value);
      if (newVal === value) {
        return;
      }
      value = newVal;
    }
  });
}

const data = {
  nums: [1, 2]
};
observer(data);
data.nums.push(3); // 视图更新

# Vue 3 Proxy

// 数据
const data = {
  text: "default"
};
const input = document.getElementById("input");
const span = document.getElementById("span");
// 数据劫持
const handler = {
  set(target, key, value) {
    target[key] = value;
    // 数据变化 --> 修改视图
    input.value = value;
    span.innerHTML = value;
    return value;
  }
};
const proxy = new Proxy(data, handler);

// 视图更改 --> 数据变化
input.addEventListener("keyup", function(e) {
  proxy.text = e.target.value;
});

# 手写虚拟 dom 过程(html 转为 json 格式)

  1. 将 HTML 字符串去<>,处理为一个数组

  2. 提取树形结构

  3. 将树形结构转 JSON

const str1 = "<div>1<span>2<a>3</a>4</span>5<span>6<a>7</a>8<a>9</a>10</span>11</div>";
function Dom2JSON(str) {
  str = str.split("<").map(x => x.split(">"));
  let res = [],
    stack = [],
    temp = {},
    cur = {},
    key = 0;
  // 获取树形结构
  for (let i = 1; i < str.length; i++) {
    if (str[i][0].indexOf("/") === -1) {
      temp = {};
      temp["key"] = key++;
      temp["tag"] = str[i][0];
      temp["value"] = str[i][1];
      temp["children"] = [];
      temp["parent"] = stack.length === 0 ? 0 : stack[0]["key"];
      stack.unshift(temp);
    } else {
      cur = stack.shift();
      // 当前元素为根元素时栈为空
      stack.length !== 0 && (stack[0]["value"] = stack[0]["value"] + cur["value"] + str[i][1]);
      res.unshift(cur);
    }
  }
  // 使得遍历时索引与key值匹配
  res = res.sort((x, y) => x["key"] - y["key"]);
  for (let i = res.length - 1; i > 0; i--) {
    temp = {};
    temp["tag"] = res[i]["tag"];
    temp["value"] = res[i]["value"];
    temp["children"] = res[i]["children"];
    res[res[i]["parent"]]["children"].unshift(temp);
  }
  res = res[0];
  delete res["parent"];
  delete res["key"];
  return JSON.parse(JSON.stringify(res));
}
console.log(Dom2JSON(str1));

// 转换结果如下
// let res ={
//     tag: "div",
//     value: "1234567891011",
//     children: [
//         {
//             tag: "span",
//             value: "234",
//             children: [
//                 {
//                     tag: "a",
//                     value: "3",
//                     children: [],
//                 }
//             ],
//         },
//         {
//             tag: "span",
//             value: "678910",
//             children: [
//                 {
//                     tag: "a",
//                     value: "7",
//                     children: [],
//                 },
//                 {
//                     tag: "a",
//                     value: "9",
//                     children: [],
//                 }
//             ]
//         }
//     ]}

# 手写一个 Ajax

一般实现:

let xhr = new XMLHttpRequest();
// readyState 为 4 和 status 为 200 的时候,是正常情况
// Step1: 监听状态
xhr.onreadystatechange = () => {
  if (xhr.readyState === 4) {
    xhr.status === 200 && console.log(xhr.responseText);
  }
};
// xhr.open(method: [get, post], url: string, async: [true, false])
// async: 默认是 true; 代表异步请求
// 如果async = false, 那么 xhr.send() 会阻塞
// Step2: 打开请求
xhr.open("GET", "http://localhost:5050/search/song?key=周杰伦&page=1&limit=10&vendor=qq", true);
// Step3: 发送请求
xhr.send();

promise 封装实现:

const ajax = (url, method, async, data) => {
  return new Promise((resolve, reject) => {
    const xhr = new XMLHttpRequest();
    xhr.onreadystatechange = () => {
      // 已经接收到全部响应数据,而且已经可以在客户端使用了
      /*0: 请求未初始化
        1: 服务器连接已建立
        2: 请求已接收
        3: 请求处理中
        4: 请求已完成,且响应已就绪*/
      if (xhr.readyState === 4) {
        if (xhr.status === 200) {
          resolve(JSON.parse(xhr.responseText));
        } else if (xhr.status > 400) {
          reject("发生错误");
        }
      }
    };
    xhr.open(url, method, async);
    xhr.send(data || null);
  });
};

# 实现一个红绿灯(3s 打印 red,2s 打印 green,1s 打印 yellow)

let setColor = function(color, delay) {
  return new Promise(resolve => {
    setTimeout(() => {
      console.log(color);
      resolve();
    }, delay);
  });
};

async function sett() {
  await setColor("red", 3000);
  await setColor("green", 2000);
  await setColor("yellow", 1000);
  await sett();
}

sett();

function red() {
  console.log("red");
}
function green() {
  console.log("green");
}
function yellow() {
  console.log("yellow");
}

var light = function(timmer, cb) {
  return new Promise(function(resolve, reject) {
    setTimeout(function() {
      cb();
      resolve();
    }, timmer);
  });
};

var step = function() {
  Promise.resolve()
    .then(function() {
      return light(3000, red);
    })
    .then(function() {
      return light(2000, green);
    })
    .then(function() {
      return light(1000, yellow);
    })
    .then(function() {
      step();
    });
};

step();

# 函数柯里化

用闭包把参数保存起来,当参数的数量足够执行函数了,就开始执行函数

function add(a, b) {
  return a + b;
}

var addCurry = curry(add, 1, 2);
addCurry(); // 3
//或者
var addCurry = curry(add, 1);
addCurry(2); // 3
//或者
var addCurry = curry(add);
addCurry(1, 2); // 3

ES6 实现

function curry(fn, ...args) {
  // 判断参数的长度是否已经满足函数所需参数的长度 够了就执行,否则新增参数然后返回函数
  return fn.length <= args.length ? fn(...args) : curry.bind(null, fn, ...args);
}

// test
let add = curry((a, b, c) => a + b + c);
console.log(add(1)(2)(3));
console.log(add(1, 2)(3));
console.log(add(1)(2, 3));

# 手写一个单例模式

单例模式的定义是:保证一个类仅有一个实例,并提供一个访问它的全局访问点

class SingleTon {
  constructor(name) {
    this.name = name;
    this.instance = null;
  }

  static getInstance(name) {
    // 新建对象时判断全局是否有该对象,如果有,就返回该对象,没有就创建一个新对象返回。
    if (!this.instance) {
      this.instance = new SingleTon(name);
    }
    return this.instance;
  }
}

var oA = SingleTon.getInstance("Lee");
var oB = SingleTon.getInstance("Fan");
console.log(oA === oB); // true

static 关键字解释:类相当于实例的原型, 所有在类中定义的方法, 都会被实例继承。 如果在一个方法前, 加上 static 关键字, 就表示该方法不会被实例继承, 而是直接通过类来调用, 这就称为“ 静态方法”。

# 手写一个观察者模式

ES6 写法:

class Event {
  constructor() {
    this.events = {};
  }
  subscribe(eventName, callback) {
    if (this.events[eventName]) {
      this.events[eventName] = [callback];
    } else {
      this.events[eventName].push(callback);
    }
  }

  publish(eventName) {
    if (this.events[eventName]) {
      this.events[eventName].forEach(callback => callback());
    }
  }

  reomve(eventName, callback) {
    if (this.events[eventName]) {
      this.events[eventName] = this.events[eventName].filter(fn => fn !== callback);
    }
  }
  reomveAll(eventName) {
    if (this.events[eventName]) {
      this.events[eventName] = [];
    }
  }
}

# 手写一个发布订阅模式

// 发布订阅模式
class EventEmitter {
  constructor() {
    // 事件对象,存放订阅的名字和事件
    this.events = {}
  }
  // 订阅事件的方法
  on(eventName, callback) {
    if (!this.events[eventName]) {
      // 注意是数据,一个名字可以订阅多个事件函数
      this.events[eventName] = [callback]
    } else {
      // 存在则push到指定数组的尾部保存
      this.events[eventName].push(callback)
    }
  }
  // 触发事件的方法
  emit(eventName) {
    // 遍历执行所有订阅的事件
    this.events[eventName] && this.events[eventName].forEach((cb) => cb())
  }
  // 移除订阅事件
  removeListener(eventName, callback) {
    if (this.events[eventName]) {
      this.events[eventName] = this.events[eventName].filter(
        (cb) => cb != callback
      )
    }
  }
  // 只执行一次订阅的事件,然后移除
  once(eventName, callback) {
    // 绑定的时fn, 执行的时候会触发fn函数
    let fn = () => {
      callback() // fn函数中调用原有的callback
      this.removeListener(eventName, fn) // 删除fn, 再次执行的时候之后执行一次
    }
    this.on(eventName, fn)
  }
}

// test
let em = new EventEmitter();
let workday = 0;
em.on("work", function() {
    workday++;
    console.log("work everyday");
});

em.once("love", function() {
    console.log("just love you");
});

function makeMoney() {
    console.log("make one million money");
}
em.on("money",makeMoney)let time = setInterval(() => {
    em.emit("work");
    em.removeListener("money",makeMoney);
    em.emit("money");
    em.emit("love");
    if (workday === 5) {
        console.log("have a rest")
        clearInterval(time);
    }
}, 1);

//  输出
//  work everyday
//  just love you
//  work everyday
//  work everyday
//  work everyday
//  work everyday
//  have a rest

# 实现一个 JSON.stringify

http://interview.poetries.top/docs/handwritten.html#_13-%E5%AE%9E%E7%8E%B0%E4%B8%80%E4%B8%AAjson-stringify (opens new window)

# 图片懒加载

function lazyload() {
  const imgs = document.getElementsByTagName("img");
  const len = imgs.length;
  // 可视窗口高度
  const viewHeight = document.documentElement.clientHeight;
  // 滚动条高度
  const scrollHeight = document.documentElement.scrollTop || document.body.scrollTop;
  for (let i = 0; i < len; i++) {
    // 元素距顶高度
    const offsetHeight = imgs[i].offsetTop;
    if (offsetHeight < viewHeight + scrollHeight) {
      const src = imgs[i].dataset.src;
      imgs[i].src = src;
    }
  }
}

// 可以使用节流优化一下
window.addEventListener("scroll", lazyload);

# 对一个页面打印所有的结点类型和结点名称

var nodes = [...document.getElementsByTagName("*")];
nodes.forEach(node => {
  console.log(node.nodeType, node.nodeName);
});

# 打印页面 HTML 种类数量

function fn() {
  return [...new Set([...document.getElementsByTagName("*")].map(el => el.tagName))].length;
}

# 场景题

# JS 树(非二叉树)结构操作

let tree = [
  {
    id: "1",
    title: "节点1",
    children: [
      {
        id: "1-1",
        title: "节点1-1"
      },
      {
        id: "1-2",
        title: "节点1-2"
      }
    ]
  },
  {
    id: "2",
    title: "节点2",
    children: [
      {
        id: "2-1",
        title: "节点2-1"
      }
    ]
  }
];

# 列表转成树

let list = [
  {
    id: "1",
    title: "节点1",
    parentId: ""
  },
  {
    id: "1-1",
    title: "节点1-1",
    parentId: "1"
  },
  {
    id: "1-2",
    title: "节点1-2",
    parentId: "1"
  },
  {
    id: "2",
    title: "节点2",
    parentId: ""
  },
  {
    id: "2-1",
    title: "节点2-1",
    parentId: "2"
  }
];
function listToTree(list = []) {
  let info = list.reduce((map, node) => {
    map[node.id] = node;
    node.children = [];
    return map;
  }, {});

  return list.filter(node => {
    if (info[node.parentId]) {
      // 寻找children
      info[node.parentId].children.push(node);
    }
    return !node.parentId;
  });
}

# 树转成列表

//递归实现
function treeToList(tree, result = [], level = 0) {
  tree.forEach(node => {
    result.push(node);
    node.level = level + 1;
    node.children && treeToList(node.children, result, level + 1);
  });
  return result;
}

// 循环实现
function treeToList(tree) {
  let node,
    result = tree.map(node => ((node.level = 1), node));
  for (let i = 0; i < result.length; i++) {
    if (!result[i].children) continue;
    let list = result[i].children.map(node => ((node.level = result[i].level + 1), node));
    result.splice(i + 1, 0, ...list);
  }
  return result;
}

# 树结构筛选

function treeFilter(tree, func) {
  // 拷贝一份树
  return tree
    .map(node => ({ ...node }))
    .filter(node => {
      // 保证子节点符合条件
      node.children = node.children && treeFilter(node.children, func);
      // 符合条件的返回
      return func(node) || (node.children && node.children.length);
    });
}

# 查找节点

function treeFind(tree, func) {
  for (let node of tree) {
    if (func(node)) {
      return node;
    }
    if (node.children) {
      const res = treeFind(node.children, func);
      if (res) return res;
    }
  }
  return null;
}

# 查找节点路径

function treeFindPath(tree, func, path = []) {
  if (!tree) return [];
  for (const data of tree) {
    path.push(data.id);
    if (func(data)) return path;
    if (data.children) {
      const findChildren = treeFindPath(data.children, func, path);
      if (findChildren.length) return findChildren;
    }
    path.pop();
  }
  return [];
}
let tree = [
  {
    id: "1",
    title: "节点1",
    children: [
      {
        id: "1-1",
        title: "节点1-1"
      },
      {
        id: "1-2",
        title: "节点1-2"
      }
    ]
  },
  {
    id: "2",
    title: "节点2",
    children: [
      {
        id: "2-1",
        title: "节点2-1"
      }
    ]
  }
];

let list = [
  {
    id: "1",
    title: "节点1",
    parentId: ""
  },
  {
    id: "1-1",
    title: "节点1-1",
    parentId: "1"
  },
  {
    id: "1-2",
    title: "节点1-2",
    parentId: "1"
  },
  {
    id: "2",
    title: "节点2",
    parentId: ""
  },
  {
    id: "2-1",
    title: "节点2-1",
    parentId: "2"
  }
];

function listToTree(list = []) {
  let info = list.reduce((map, node) => {
    map[node.id] = node;
    node.children = [];
    return map;
  }, {});

  return list.filter(node => {
    if (info[node.parentId]) {
      // 寻找children
      info[node.parentId].children.push(node);
    }
    return !node.parentId;
  });
}

// 筛选

function treeFilter(tree, func) {
  // 拷贝一份树
  return tree
    .map(node => ({ ...node }))
    .filter(node => {
      // 保证子节点符合条件
      node.children = node.children && treeFilter(node.children, func);
      // 符合条件的返回
      return func(node) || (node.children && node.children.length);
    });
}

// 查找节点

function treeFind(tree, func) {
  for (let node of tree) {
    if (func(node)) {
      return node;
    }
    if (node.children) {
      const res = treeFind(node.children, func);
      if (res) return res;
    }
  }
  return null;
}

var filter = node => node.id === "1";

console.log(listToTree(list));

console.log(tree.map(node => ({ ...node })));

console.log(treeFilter(tree, filter));

console.log(treeFind(tree, filter));

img

# 写一个 mySetInterVal(fn, a, b),每次间隔 a,a+b,a+2b 的时间,然后写一个 myClear,停止上面的 mySetInterVal

function mySetInterVal(fn, a, b) {
  this.a = a;
  this.b = b;
  this.time = 0;
  this.handle = -1;
  this.start = () => {
    this.handle = setTimeout(() => {
      fn();
      this.time++;
      this.start();
      console.log(this.a + this.time * this.b);
    }, this.a + this.time * this.b);
  };

  this.stop = () => {
    clearTimeout(this.handle);
    this.time = 0;
  };
}

var a = new mySetInterVal(
  () => {
    console.log("123");
  },
  1000,
  2000
);
a.start();
a.stop();

# 实现 lodash 的 chunk 方法--数组按指定长度拆分

/**
 * @param input
 * @param size
 * @returns {Array}
 */
_.chunk(["a", "b", "c", "d"], 2);
// => [['a', 'b'], ['c', 'd']]

_.chunk(["a", "b", "c", "d"], 3);
// => [['a', 'b', 'c'], ['d']]

_.chunk(["a", "b", "c", "d"], 5);
// => [['a', 'b', 'c', 'd']]

_.chunk(["a", "b", "c", "d"], 0);
// => []
function chunk(arr, length) {
  let newArr = [];
  for (let i = 0; i < arr.length; i += length) {
    newArr.push(arr.slice(i, i + length));
  }
  return newArr;
}

# 实现字符串首字母大写

"hello world".toLowerCase().replace(/( |^)[a-z]/g, str => str.toUpperCase());

// Hello World

# 数组转为树

要求将 [{id:1, parentId: 0}, {id:2, parentId:1},{id:3, parentId:1}] 输出成树结构

[{id:1, parentId: 0,children:[{id:2, parentId:1},{id:3, parentId:1}]}]

var list = [
  { id: 1, name: "部门A", parentId: 0 },
  { id: 3, name: "部门C", parentId: 1 },
  { id: 4, name: "部门D", parentId: 1 },
  { id: 5, name: "部门E", parentId: 2 },
  { id: 6, name: "部门F", parentId: 3 },
  { id: 7, name: "部门G", parentId: 2 },
  { id: 8, name: "部门H", parentId: 4 }
];
function convert(list) {
  const map = list.reduce((acc, item) => {
    acc[item.id] = item;
    return acc;
  }, {});
  const result = [];
  for (const key in map) {
    const item = map[key];
    if (item.parentId === 0) {
      result.push(item);
    } else {
      const parent = map[item.parentId];
      if (parent) {
        parent.children = parent.children || [];
        parent.children.push(item);
      }
    }
  }
  return result;
}
var result = convert(list);

# 输出结果题集

function Foo() {
  getName = function() {
    console.log(1);
  };
  return this;
}
var getName;
function getName() {
  console.log(5);
}
Foo.getName = function() {
  console.log(2);
};
Foo.prototype.getName = function() {
  console.log(3);
};
getName = function() {
  console.log(4);
};

Foo.getName(); // ? 2
getName(); // ? 4
Foo().getName(); // ?
getName(); // ?
new Foo.getName(); // ?
new Foo().getName(); // ?
new new Foo().getName(); // ?
async function async1() {
  console.log("async1 start");
  await async2();
  console.log("async1 end");
}
async function async2() {
  console.log("async2");
}
console.log("script start");
setTimeout(function() {
  console.log("setTimeout");
}, 0);
async1();
new Promise(function(resolve) {
  console.log("promise1");
  resolve();
}).then(function() {
  console.log("promise2");
});
console.log("scripts end");
// 写出代码执行完成打印的结果
var a = {
  name: "A",
  fn() {
    console.log(this.name);
  }
};
a.fn();
a.fn.call({ name: "B" });
var fn1 = a.fn;
fn1();
// 写出打印结果
let int = 1;
setTimeout(function() {
  console.log(int);
  int = 2;
  new Promise((resolve, reject) => {
    resolve();
  }).then(function() {
    console.log(int);
    int = 7;
  });
  console.log(int);
});
int = 3;
console.log(int);
new Promise((resolve, reject) => {
  console.log(int);
  return resolve((int = 4));
}).then(function(res) {
  console.log(int);
  int = 5;
  setTimeout(function() {
    console.log(int);
    int = 8;
  });
  return false;
});
console.log(int);
// 写出打印结果
function a(obj) {
  obj.a = 2;
  obj = { a: 3 };
  return obj;
}
const obj = { a: 1 };
a(obj);
console.log(obj);
Function.prototype.a = () => {
  console.log(1);
};
Object.prototype.b = () => {
  console.log(2);
};
function A() {}
const a = new A();
console.log(a.__proto__, A.prototype.__proto__);
a.a();
a.b();
// 写出执行结果
let a = 0;
console.log(a);
console.log(b);
let b = 0;
console.log(c);
function c() {}
// 写出执行结果
var x = 10;
function a(y) {
  var x = 20;
  return b(y);
}
function b(y) {
  return x + y;
}
a(20);
// 写出执行结果
console.log(1);
setTimeout(() => {
  console.log(2);
});
process.nextTick(() => {
  console.log(3);
});
setImmediate(() => {
  console.log(4);
});
new Promise(resolve => {
  console.log(5);
  resolve();
  console.log(6);
}).then(() => {
  console.log(7);
});
Promise.resolve().then(() => {
  console.log(8);
  process.nextTick(() => {
    console.log(9);
  });
});
// 写出执行结果
[1, 2, 3, 4, 5].map(parselnt);
// 写出执行结果
typeof typeof typeof [];
// 写出执行结果
function Foo() {
  getName = function() {
    console.log(1);
  };
  return this;
}
Foo.getName = function() {
  console.log(2);
};
Foo.prototype.getName = function() {
  console.log(3);
};
getName = function() {
  console.log(4);
};

// 请写出下面的输出结果
getName();
Foo.getName();
new Foo().getName();
var fullname = "Test1";
var obj = {
  fullname: "Test2",
  prop: {
    fullname: "Test3",
    getFullname: function() {
      return this.fullname;
    }
  }
};
console.log(obj.prop.getFullname());
var test = obj.prop.getFullname;
console.log(test());

# 给定一个数组,按找到每个元素右侧第一个比它大的数字,没有的话返回-1 规则返回一个数组

/*
 *示例:
 *给定数组:[2,6,3,8,10,9]
 *返回数组:[6,8,8,10,-1,-1]
 */
function handler(arr = []) {
  const result = [];
  for (let i = 0, len = arr.length; i < len - 1; i++) {
    for (let j = i + 1; j < len; j++) {
      if (arr[j] > arr[i]) {
        result[i] = arr[j];
        break;
      }
      result[i] = -1;
    }
  }
  result[arr.length - 1] = -1;
  return result;
}
console.log(handler([2, 6, 3, 8, 10, 9]));

# 输出一个随机的 16 进制颜色

var color =
  "#" +
  Math.random()
    .toString(16)
    .substr(-6);
document.body.style.backgroundColor = color;

# 设计一个函数,奇数次执行的时候打印 1,偶数次执行的时候打印 2

function countFn() {
  let count = 0;
  return function(...args) {
    count++;
    if (count & (1 === 1)) return console.log(1);
    console.log(2);
  };
}
const testFn = countFn();
testFn(); // 1
testFn(); // 2
testFn(); // 1
testFn(); // 2
testFn(); // 1
testFn(); // 2

# 给定起始日期,输入出之前的日期

// 输入两个字符串 2018-08  2018-12
// 输出他们中间的月份 [2018-10, 2018-11]
// 给定起止日期,返回中间的所有月份
function getDate(dateStr = "", addMonth = 0) {
  const [year, month] = dateStr.split("-");
  return new Date(year, month - 1 + addMonth);
}
function formateDate(dateStr = new Date()) {
  return `${dateStr.getFullYear()}-${String(dateStr.getMonth() + 1).padStart(2, 0)}`;
}
const getRangeMonth = (startDateStr = "", endDateStr = "") => {
  const result = [];
  let startTime = getDate(startDateStr) ? getDate(startDateStr, 1).getTime() : 0;
  const endTime = getDate(endDateStr) ? getDate(endDateStr).getTime() : 0;
  while (startTime < endTime) {
    const curTime = new Date(startTime);
    result.push(formateDate(curTime));
    curTime.setMonth(curTime.getMonth() + 1);
    startTime = curTime.getTime();
  }
  return result;
};
console.log(getRangeMonth("2018-08", "2018-12")); // [ '2018-09', '2018-10', '2018-11' ]

# 请写一个函数,输出出多级嵌套结构的 Object 的所有 key 值

var obj = {
  a: "12",
  b: "23",
  first: {
    c: "34",
    d: "45",
    second: { 3: "56", f: "67", three: { g: "78", h: "89", i: "90" } }
  }
};
// => [a,b,c,d,e,f,g,h,i]
var obj = {
  a: "12",
  b: "23",
  first: {
    c: "34",
    d: "45",
    second: { 3: "56", f: "67", three: { g: "78", h: "89", i: "90" } }
  }
};
// => [a,b,c,d,e,f,g,h,i]
function getAllKeys(obj = {}, res = []) {
  if (typeof obj !== "object" || obj === null) return [];
  for (const key in obj) {
    if (obj.hasOwnProperty(key)) {
      if (obj[key] && typeof obj[key] === "object") {
        return getAllKeys(obj[key], res);
      }
      res.push(key);
    }
  }
  return res;
}
console.log(getAllKeys(obj));

# 写出打印结果,并解释为什么

var a = { k1: 1 };
var b = a;
a.k3 = a = { k2: 2 };
console.log(a);
console.log(b);
{k2:2} {k1:1,k3:{k2:2}}
第一步 a被赋值{k1:1},设为对象A
第二步 b浅拷贝a,地址不变
第三步 .运算符优先级最高,所以a:{k1:1,k3:{k2:2}},b:{k1:1,k3:{k2:2}}
第四步 a被赋值 a:{k2:2}
所以输出以上结果

# versions 是一个项目的版本号列表,因多人维护,不规则,动手实现一个版本号处理函数

var versions = ["1.45.0", "1.5", "6", "3.3.3.3.3.3.3"];
// 要求从小到大排序,注意'1.45'比'1.5'大
function sortVersion(versions) {
  // TODO
}
// => ['1.5','1.45.0','3.3.3.3.3.3','6']
var versions = ["1.45.0", "1.5", "6", "3.3.3.3.3.3.3"];
// 要求从小到大排序,注意'1.45'比'1.5'大
function sortVersion(versions) {
  if (!versions || !versions.length) return [];
  const result = versions.sort((a, b) => {
    const arrA = a.split("."),
      arrB = b.split(".");
    const length = Math.max(a.length, b.length);
    for (let i = 0; i < length; i++) {
      const x = Number(arrA[i] || 0);
      const y = Number(arrB[i] || 0);
      if (x - y !== 0) return x - y;
    }
  });
  return result;
}
console.log(sortVersion(versions)); // [ '1.5', '1.45.0', '3.3.3.3.3.3.3', '6' ]

# 实现 input 框的 autocomplete 属性

window.onload = () => {
  const input = document.getElementById("input");
  const words = ["珠海", "广州", "上海", "杭州", "成都"];
  input.addEventListener(
    "input",
    debounce(function(e) {
      const value = e.target.value;
      // 保证value只存在words里面的值
      const index = words.findIndex(item => value && item.indexOf(value) !== -1);
      if (index !== -1) {
        e.target.value = words[index];
      }
    }, 500)
  );

  function debounce(fn, wait = 500) {
    let timeout;
    return function() {
      if (timeout) {
        clearTimeout(timeout);
      }
      timeout = setTimeout(() => {
        fn.apply(this, [...arguments]);
      }, wait);
    };
  }
};

# 手写 trim()函数,去掉首尾空格

Function.prototype.trim = function() {
  return this.replace(/^\s+/, "").replace(/\s+$/, "");
};

# 对输入的字符串,去除其中的字符'b'以及连续出现的'a'和'c'

  'aacbd' -> 'ad'
  'aabcd' -> 'ad'
  'aaabbccc' -> ''

function fn(str) {
  let res = str.replace(/b/g, "");
  while (res.match(/(ac)+/)) {
    res = res.replace(/ac/, "");
  }
  return res;
}

# 时间复杂度为 O(n)的情况下随机不重复输出 1~100

let a = [];
for (let i = 0; i < 100; i++) {
  a[i] = i + 1;
}
a.sort(() => 0.5 - Math.random());
console.log(a);

# JavaScript:字符串内字母排序(升序和降序)

var s = "kadfjkajfkhgofqnmvc";
console.log(
  Array.from(s)
    .sort()
    .join("")
); //aacdfffghjjkkkmnoqv
console.log(
  Array.from(s)
    .sort()
    .reverse()
    .join("")
);

# 打印 1-100 以内的质数

质数,大于 1 的正整数满足除自身外,无其他因数的数

function isPrimeNumber(x) {
  var tmp = true;
  // 因数从2开始取
  for (let i = 2; i < x; i++) {
    if (x % i === 0) {
      tmp = false;
      break;
    }
  }
  return x > 1 && tmp;
}
let res = [];
for (let i = 1; i <= 100; i++) {
  if (isPrimeNumber(i)) {
    res.push(i);
  }
}
console.log(res);

# 异步调度器

JS 实现一个带并发限制的异步调度器 Scheduler,保证同时运行的任务最多有两个。完善代码中 Scheduler 类,使得以下程序能正确输出。

class Scheduler {
  add(promiseCreator) { ... }
  // ...
}

const timeout = (time) => new Promise(resolve => {
  setTimeout(resolve,time)
}

const scheduler = new Scheduler()
const addTask = (time, order) => {
  scheduler.add(() => timeout(time))
    .then(() => console.log(order))
}

addTask(1000, '1')
addTask(500, '2')
addTask(300, '3')
addTask(400, '4')// output: 2 3 1 4// 一开始,1、2两个任务进入队列// 500ms时,2完成,输出2,任务3进队// 800ms时,3完成,输出3,任务4进队// 1000ms时,1完成,输出1// 1200ms时,4完成,输出4

class Scheduler {
  constructor() {
    this.count = 0;
    this.waitQueue = [];
  }
  add(promiseCreator) {
    if (this.count < 2) {
      this.count += 1;
      return this.run(promiseCreator);
    } else {
      return new Promise(resolve => {
        this.waitQueue.push(() => promiseCreator().then(resolve));
      });
    }
  }
  run(promiseCreator) {
    return promiseCreator().then(() => {
      this.count -= 1;
      if (this.waitQueue.length) {
        this.run(this.waitQueue.shift());
      }
    });
  }
}

# js 实现简易版模板引擎

function compile(tpl, data) {
  // data必须要是{name:value} 形式 ,key作为模板变量
  const regex = /\{\{([^}]*)\}\}/g;
  const string = tpl.trim().replace(regex, function(match, $1) {
    if ($1) {
      return data[$1];
    } else {
      return "";
    }
  });
  return string;
}

compile(`${name}+的${box}`, { name: "杨", box: "手机" });

# 给你一个连续的数字字符串,要求从尾部 3 个一个的输出,例如 12345678 输出 12,345,678

两种方法取一即可

function localString(str) {
  // 每次将模1000的余数从首部推入数组 然后再将数字除以1000向下取整
  let num = Number(str);
  let arr = [];
  while (num > 0) {
    arr.unshift(num % 1000);
    num = Math.floor(num / 1000);
  }
  return arr.join(",");
}

let num = 12345678;
console.log(num.toLocaleString());

# 输出一个随机字符串

function randomString(length) {
  var str = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
  var result = "";
  for (var i = length; i > 0; --i) result += str[Math.floor(Math.random() * str.length)];
  return result;
}
alert(randomString(6));

# 判断是否是电话号码

function isPhone(tel) {
  var regx = /^1[34578]\d{9}$/;
  return regx.test(tel);
}

# 验证 ip 地址的合法性

function isIP(ip) {
  var reSpaceCheck = /^(\d+)\.(\d+)\.(\d+)\.(\d+)$/;
  if (reSpaceCheck.test(ip)) {
    ip.match(reSpaceCheck);
    if (
      RegExp.$1 <= 255 &&
      RegExp.$1 >= 0 &&
      RegExp.$2 <= 255 &&
      RegExp.$2 >= 0 &&
      RegExp.$3 <= 255 &&
      RegExp.$3 >= 0 &&
      RegExp.$4 <= 255 &&
      RegExp.$4 >= 0
    ) {
      return true;
    } else {
      return false;
    }
  } else {
    return false;
  }
}

# 计算字符出现次数最多的前数字之和

// 给定数组 ['111a','2b','13c','5a'] ,输出出现次数最多的字母前数字之和 6。
function f(arr) {
  let res = 0;
  let stringArray = arr.map(item => item.replace(/\d+/, ""));
  let map = new Map();
  for (let item of stringArray) {
    map.set(item, map.has(item) ? map.get(item) + 1 : 1);
  }
  let s = Array.from(map).sort((a, b) => b[1] - a[1])[0][0];
  let numArray = arr.map(item => {
    if (item.match(s)) {
      // 取出字符串里面的数字
      return parseInt(item.replace(/[^0-9]/gi, ""));
    } else {
      return 0;
    }
  });
  return numArray.reduce((prev, cur) => prev + cur);
}
console.log(f(["1111a", "2b", "13c", "5a", "7a"]));

# 查找字符串中出现最多的字符和个数

let s = "abcabcabcbbccccc";
var f = str => {
  let map = new Map();
  for (let c of str) {
    map.set(c, map.has(c) ? map.get(c) + 1 : 1);
  }

  for (let [key, value] of map) {
    if (value === Math.max(...[...map.values()])) {
      console.log(key + "=" + value);
    }
  }
};
f(s);

# 将数字转为中文

function transform(num) {
  var cNums = "零一二三四五六七八九";
  var chineseUnits = "个十百千万";
  var numArr = num.toString().split("");
  var lastIndex = numArr.length - 1;

  var chineseStr = numArr.reduceRight((result, n, index) => {
    var number = cNums[n];
    var unit = index !== lastIndex && num !== 0 ? chineseUnits[lastIndex - index] : "";
    return number + unit + result;
  }, "");
  // 最后去考虑多余的零
  return chineseStr.replace(/零(?=零)|(零+$)/g, "");
}

# 判断循环引用

// 判断循环引用 循环引用代表父级子元素的值等于父级
let isCycle = false;
function cycle(obj, parent) {
  // 定义父级数组
  var parentArr = parent || [obj];
  for (let key in obj) {
    if (typeof obj[key] === "object") {
      // 判断是否存在和子集相同
      parentArr.forEach(item => {
        if (item === obj[key]) {
          obj[key] = "[cycle]";
          isCycle = true;
        }
      });
      cycle(obj[key], [...parentArr, obj[key]]);
    }
  }
  return isCycle;
}

var a = {
  b: {
    c: {}
  }
};

a.b.c.d = a;

console.log(cycle(a));

# 实现一个延时队列 Queue

class Queue {
  constructor() {
    this.event = [];
  }
  task(fn, time) {
    function callback() {
      return new Promise(resolve => {
        setTimeout(() => {
          fn();
          resolve();
        }, time);
      });
    }
    this.event.push(callback);
    // 最后返回实例
    return this;
  }
  start() {
    for (let item of this.event) {
      item();
    }
  }
}

var queue = new Queue();
queue
  .task(() => {
    console.log(1);
  }, 100)
  .task(() => {
    console.log(2);
  }, 200)
  .task(() => {
    console.log(3);
  }, 300)
  .start();

# 如何做到更新路有参数,但是不刷新页面

https://developer.mozilla.org/zh-CN/docs/Web/API/History_API

if (window.history) {
  history.replace(null, "", "url");
}

# 编写一个阶乘

function fnc(n) {
  if (n === 1) {
    return 1;
  }
  return n * fnc(n - 1);
}

# 实现仿百度输入框,带有输入提示

原生写法

<!DOCTYPE html>
<html lang="zh-CN">
  <head>
    <meta charset="UTF-8" />
    <title>Document</title>
  </head>

  <style>
    #kw {
      width: 284px;
      height: 30px;
      border: 2px solid #333333;
      line-height: 30px;
      font-size: 16px;
      box-sizing: border-box;
      padding: 0 5px;
    }
    #append {
      width: 286px;
      box-sizing: border-box;
      border: 2px solid #333333;
      border-top: 0;
      display: none;
    }
    #append .item:hover {
      background-color: aqua;
      cursor: pointer;
    }
    .item {
      padding: 3px 5px;
      cursor: pointer;
    }
  </style>

  <body>
    <div id="content">
      <input id="kw" onkeyup="getContent(this);" placeholder="请输入" />
      <div id="append"></div>
    </div>
  </body>

  <script>
    let data = [
      "你好,我是Michael",
      "你是谁",
      "你最好啦",
      "你最珍贵",
      "你是我最好的朋友",
      "你画我猜",
      "你是笨蛋",
      "你懂得",
      "你为我着迷",
      "你是我的眼"
    ];
    /**
     * let xhr = new xmlHttpRequest()
     * xhr.open('GET','url',true)
     * xhr.onreadystatechange=()=>{
     * if(xhr.readyState === 4){
     * xhr.status === 200 &&& console.log(xhr.responseText)
     * }
     * }
     * xhr.send()
     *
     * xhr.open('POST','url',true)
     *
     * xhr.setRequestHeader('Content-type','application/x-www-form-urlencoded')
     *
     * xhr.send('a=1&b=2')
     * */

    let input = document.getElementById("kw");
    let append = document.getElementById("append");

    function getContent(obj) {
      let inputValue = obj.value.trim();
      // 首先为空值的情况,append不展示
      if (inputValue === "") {
        append.style.display = "none";
        append.innerHTML = "";
        return;
      }
      // 有值的情况下,遍历data,如果命中里面的数据,就创建一个item的html,并且么个都绑定点击事件
      let html = "";
      for (let i = 0; i < data.length; i++) {
        if (data[i].indexOf(inputValue)) {
          html += "<div class='item' onClick='getCon(this);'>" + data[i] + "</div>";
        }
      }
      if (html !== "") {
        append.style.display = "block";
        append.innerHTML = html;
      }
    }
    function getCon(obj) {
      input.value = obj.innerText;
      append.style.display = "none";
      append.innerHTML = "";
    }
  </script>
</html>

# 实现一个弹窗组件

Vue 写法

弹窗层组件分为: 遮罩层 + 内容层

<template>
  <div class="modal-background" v-show="show">
    <div class="modal-wrapper">
      <div class="modal-header">
        {{ title }}
      </div>
      <div class="modal-content">
        <slot></slot>
      </div>
      <div class="modal-footer">
        <button type="button" @click="confirm" class="btn  confirm">确定</button>
        <button type="button" @click="cancel" class="btn cancel">取消</button>
      </div>
    </div>
  </div>
</template>

<script>
export default {
  name: "",
  props: {
    show: {
      type: Boolean,
      default: false
    },
    title: {
      type: String,
      default: "弹窗标题"
    }
  },
  methods: {
    confirm() {
      this.$emit("onConfirm");
    },
    cancel() {
      this.$emit("onCancel");
    }
  }
};
</script>
<style scoped>
.modal-background {
  position: fixed;
  left: 0;
  top: 0;
  bottom: 0;
  width: 100%;
  height: 100%;
  background: rgba(0, 0, 0, 0.5);
  z-index: 10;
}
.modal-wrapper {
  display: flex;
  flex-direction: column;
  width: 400px;
  height: 200px;
  position: fixed;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
}
.modal-header {
  height: 60px;
  line-height: 60px;
  background: #ffffff;
  border-bottom: 1px solid #000000;
}
.modal-footer {
  display: flex;
  justify-content: center;
  align-items: center;
  padding-bottom: 20px;
}

.btn {
  display: inline-block;
  text-align: center;
  vertical-align: middle;
  font-size: 14px;
  font-weight: 400;
  padding: 12px 20px;
  border: 1px solid transparent;
  line-height: 1;
  border-radius: 4px;
  cursor: pointer;
  user-select: none;
  outline: none;
}

.confirm {
  background: #007bff;
  color: #ffffff;
  border-color: #007bff;
  margin-right: 15px;
}
.cancel {
  border-color: #dcdfe6;
  background-color: #fff;
  color: #606266;
}
</style>

使用组件

# 手撕组件(分页器,搜索框,弹窗,评分,日期选择器组件)

# 异步加载图片

// 异步加载图片
function loadImage(url) {
    return new Promise((resolve, reject) => {
        const image = new Image();
        image.src = url;
        image.onload = function () {
            document.body.appendChild(image);
            resolve('ok');
        }
    })
}
let p = Promise.resolve();
const imgs = [url1, url2, url3, ..., urln];
for (let url of imgs) {
    p.then(() => loadImage(url))
}

# 手写倒计时

倒计时
截止时间 202011110点
显示“剩余XXXXXXXX秒”
每秒刷新一次
let deadline = new Date('2020/11/11 00:00:00').getTime();
window.setInterval(getDeadline, 1000);

function getDeadline() {
    let cur = Date.now();
    let time = deadline - cur;
    let day = parseInt(time / 1000 / 3600 / 24);
    let hour = parseInt((time - day * 24 * 3600 * 1000) / 1000 / 3600);
    let min = parseInt((time - day * 24 * 3600 * 1000 - hour * 3600 * 1000) / 1000 / 60);
    let sec = parseInt((time - day * 24 * 3600 * 1000 - hour * 3600 * 1000 - min * 60 * 1000) / 1000);
    // 都显示成两位数 1 -> 01
    day = ('00' + day).slice(-2);
    hour = ('00' + hour).slice(-2);
    min = ('00' + min).slice(-2);
    sec = ('00' + sec).slice(-2);
    console.log(`剩余${day}${hour}${min}${sec}`);
}

# Lodash.get

function get(object, path, defaultValue) {
  if (!Array.isArray(path) && typeof path !== "string")
    throw new TypeError(`path must be array or string, but you pass the type ${typeof path}`);

  // object 的类型如果是 null 或者 undefined 直接返回 default value
  if (object == null) return defaultValue;

  let propArray = path;
  if (typeof path === "string") {
    // 如果 path 是字符串需要解析成 properties 数组,这里只是简单的替换 [expression] 成 .expression 后再根据 . 拆分成特性数组
    // 其实 lodash 源码比这复杂的多
    path = path.replace(/\[(\w*)\]/g, ".$1");
    path = path.startsWith(".") ? path.slice(1) : path;
    propArray = path.split(".");
  }

  let index = 0;
  let property;
  const { length } = propArray;
  // 利用特性数组循环赋值, 取到 undefined 或者 null 就停止循环
  // 有可能是中途某个属性本身是 null 或 undefined,也有可能是根本没有当前 property 这个属性
  while (object != null && index < length) {
    property = propArray[index++];
    object = object[property];
  }

  return index === length ? object : defaultValue;
}

# 反转数字

-321 --> -123 1200 --> 21

var f = function(str) {
  if (Number(str) < 0) {
    return (
      "-" +
      str
        .split("-")[1]
        .split("")
        .reverse()
        .join("")
        .replace(/\b(0)+/g, "")
    );
  } else {
    return str
      .split("")
      .reverse()
      .join("")
      .replace(/\b(0)+/g, "");
  }
};
console.log(f("-321"), f("1200"));

# 正则提取

提取数字....value.replace(/[^\d]/g,'')
提取中文....value.replace(/[^\u4E00-\u9FA5]/g,'')
提取英文.....value.replace(/[^a-zA-Z]/g,'')
Last update: 3/6/2022, 9:03:36 AM