January 02, 2021     38min read

interview code questions

手写一个闭包

闭包小案例

for (var i = 1; i < 5; i++) {
  (function (j) {
    setTimeout(function timer() {
      console.log(j)
    }, j * 1000)
  })(i)
  //闭包的形式解决问题
  //输出为1,2,3,4
}
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

闭包的简单实现

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

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

函数节流与抖动

scroll 事件本身会触发页面的重新渲染,同时 scroll 事件的 handler 又会被高频度的触发, 因此事件的 handler 内部不应该有复杂操作,例如 DOM 操作就不应该放在事件处理中。

针对此类高频度触发事件问题(例如页面 scroll ,屏幕 resize,监听用户输入等),下面介绍两种常用的解决方法,防抖和节流。

函数抖动:在事件被触发 n 秒后再执行回调,如果在这 n 秒内事件又被触发,则重新计时。

// 函数防抖的实现
function debounce(fn, wait) {
  var timer = null

  return function () {
    var context = this,
      args = arguments

    // 如果此时存在定时器的话,则取消之前的定时器重新记时
    if (timer) {
      clearTimeout(timer)
      timer = null
    }

    // 设置定时器,使事件间隔指定事件后执行
    timer = setTimeout(() => {
      fn.apply(context, args)
    }, wait)
  }
}
// test
var debounceRun = debounce(function () {
  console.log(123)
}, 2000)
// 只有当鼠标移动停止后2s打印123
window.addEventListener('mousemove', debounceRun)

函数节流:规定一个单位时间,在这个单位时间内,只能有一次触发事件的回调函数执行,如果在同一个单位时间内某事件被触发多次,只有一次能生效。

// 函数节流的实现;
function throttle(fn, delay) {
  var preTime = Date.now()
  return function () {
    var nowTime = Date.now()
    if (nowTime - preTime >= delay) {
      preTime = nowTime
      fn.apply(this, arguments)
    }
  }
}
// test
var throttleRun = throttle(() => {
  console.log(123)
}, 2000)
// 不停的移动鼠标,控制台每隔2s就会打印123
window.addEventListener('mousemove', throttleRun)

手写一个 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)
})

方法三:利用 async

function sleep(ms) {
  return new Promise((resolve) => {
    setTimeout(resolve, ms)
  })
}

async function init() {
  var temp = await sleep(2000)
  console.log(111) //2s后执行
}

init()

实现一下继承 call apply bind

实现 call

来看下 call 的原生表现形式:

var foo = {
  value: 1,
}

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

bar.call(foo) // 1

从上面代码的执行结果,我们可以看到,call 首先改变了 this 的指向,使函数的 this 指向了 foo,然后使 bar 函数执行了。

总结如下:

  1. call 改变函数 this 指向,
  2. 调用函数

思考一下:我们如何实现上面的效果呢?代码改造如下:

//将bar函数挂载到foo对象上,使其成为foo的方法,用foo.bar来调用
var foo = {
  value: 1,
  bar: function () {
    console.log(this.value)
  },
}
foo.bar() //1

为了模拟 call 方法,我们可以这样做:

  1. 将函数设为某个对象的属性(或者方法)
  2. 通过该对象的属性调用该函数
  3. 删除该对象上的这个属性(或者方法)

代码如下:

Function.prototype.myCall = function (context) {
  const fn = Symbol('fn')        // 声明一个独有的Symbol属性, 防止fn覆盖已有属性
  context = context || window // 若没有传入this, 默认绑定window对象
  context.fn = this // 将函数挂载到对象的fn属性上
  const args = [...arguments].slice(1) // 处理传入的参数
  const result = context.fn(...args) // 通过对象的属性调用该方法
  delete context.fn // 删除该属性
  return result
}

// 测试
function test(arg1, arg2) {
  console.log(arg1, arg2)
  console.log(this.a, this.b)
}

test.myCall(
  {
    a: 'a',
    b: 'b',
  },
  1,
  2
)

我们看一下上面的代码:

  1. 首先我们对参数 context 做了兼容处理,不传值,context 默认值为 window。
  2. 然后我们将函数挂载到 context 上面,context.fn = this;
  3. 处理参数,将传入 myCall 的参数截取,去除第一位,然后转为数组;
  4. 调用 context.fn,此时 fn 的 this 指向 context;
  5. 删除对象上的属性 delete context.fn
  6. 将结果返回。

实现 apply

apply 和 call 实现类似,只是传入的参数形式是数组形式,而不是逗号分隔的参数序列。

因此,借助 es6 提供的...运算符,就可以很方便的实现数组和参数序列的转化。

Function.prototype.myApply = function (context) {
  const fn = Symbol('fn')        // 声明一个独有的Symbol属性, 防止fn覆盖已有属性
  context = context || window // 若没有传入this, 默认绑定window对象
  context.fn = this // 将函数挂载到对象的fn属性上
  const args = [...arguments].slice(1) // 处理传入的参数
  const result = context.fn(args) // 通过对象的属性调用该方法
  delete context.fn // 删除该属性
  return result
}

// 测试
function test(arr) {
  console.log(arr)
  console.log(this.a, this.b)
}

test.myApply(
  {
    a: 'a',
    b: 'b',
  },
  [1, 2]
)

实现 bind

在模拟 bind 的实现之前,先看一下 bind 的使用案例:

var obj = { a: 1 }
function bar() {
  console.log(this.a)
}
bar.bind(obj)() //here

bind 函数虽然也能改变 bar 函数的 this,但是改变后,函数并不会执行,只是返回一个新的函数,想执行就得继续调用,仔细观察第五行代码的写法。

根据上面的使用案例,我们先实现一个简单版本的 bind:

Function.prototype.myBind = function (context) {
  return () => {
    // 要用箭头函数,否则 this 指向错误
    return this.call(context)
  }
}
var obj = { a: 1 }
function bar() {
  console.log(this.a)
}
bar.myBind(obj)()

但是这样比较简陋,函数的参数一多就不能处理了,如下面这种情况:

bar.bind(obj, 2)(2)
// or
bar.bind(obj)(2, 2)

为了兼容 bind 调用时满足参数传递的不同方式,代码修改如下:

Function.prototype.myBind = function (ctx, ...arg1) {
  return (...arg2) => {
    return this.call(ctx, ...arg1, ...arg2)
  }
}
//测试代码
var obj = { a: 1 }
function bar(b, c) {
  console.log(this.a + b + c)
}
bar.myBind(obj)(20, 30)
bar.myBind(obj, 20, 30)()

实现一个forEach方法

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

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

//currentValue  必需。当前元素
//currentIndex  可选。当前元素的索引
//arr           可选。当前元素所属的数组对象。
//thisArg       可选参数。当执行回调函数时,用作 this 的值。
Array.prototype._forEach = function(fn, thisArg) {
    if (typeof fn !== 'function') throw "参数必须为函数";
    if(!Array.isArray(this)) throw "只能对数组使用forEach方法";
    let arr = 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

function Counter() {
    this.sum = 0;
    this.count = 0;
}
// 因为 thisArg 参数(this)传给了 forEach(),每次调用时,它都被传给 callback 函数,作为它的 this 值。
Counter.prototype.add = function (array) {
    array._forEach(function (entry) {
        this.sum += entry;
        ++this.count;
    }, this);
      // ^---- Note
};

const obj = new Counter();
obj.add([2, 5, 9]);

console.log(obj.count); // 3 === (1 + 1 + 1)
console.log(obj.sum);  // 16 === (2 + 5 + 9)

用reduce实现map

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

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

//total 必需。初始值, 或者计算结束后的返回值。
//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]

['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的情况见如下解释
parseInt('2', 1); // radix基数只能取到 2 - 36 之间,所以NaN
parseInt('3', 2); // radix=2 表示是二进制数,只能有0和1,解析的字符串是'3',所以是NaN
如果 radix 是 undefined、0或未指定的,JavaScript会假定以下情况:

如果输入的 string以 "0x"或 "0x"(一个0,后面是小写或大写的X)开头,那么radix被假定为16,字符串的其余部分被当做十六进制数去解析。
如果输入的 string以 "0"(0)开头, radix被假定为8(八进制)或10(十进制)。具体选择哪一个radix取决于实现。ECMAScript 5 澄清了应该使用 10 (十进制),但不是所有的浏览器都支持。因此,在使用 parseInt 时,一定要指定一个 radix。
如果输入的 string 以任何其他值开头, radix 是 10 (十进制)。

手写一个instanceof

function instanceofFunc(obj, cons) {
  // 错误判断 构造函数必须是一个function 其他的均报错
  if (typeof cons !== 'function') throw new Error('instance error')
  if (!obj || (typeof obj !== 'object' && typeof obj !== 'function')) return false
  // 获取到原型对象
  let proto = cons.prototype
  // 如果obj的原型对象不是null
  while (obj.__proto__) {
    if (obj.__proto__ === proto) return true
    obj = obj.__proto__
  }
  return false
}

console.log(instanceofFunc(() => {}, Function)) // true

手写一个 new

new 的作用:

  1. 首先创建了一个新的空对象
  2. 设置原型,将对象的原型设置为函数的 prototype 对象。
  3. 让函数的 this 指向这个对象,执行构造函数的代码(为这个新对象添加属性)
  4. 判断函数的返回值类型,如果是值类型,返回创建的对象。如果是引用类型,就返回这个引用类型的对象。

实现:

function myNew() {
  // 1.创建空对象
  let obj = {}
  let constructor = [...arguments][0]
  let params = [...arguments].slice(1)

  // 2.空对象的原型指向构造函数的原型
  obj.__proto__ = constructor.prototype

  // 3.执行构造函数的代码
  var ret = constructor.apply(obj, params)

  // 4.判断返回值类型:
  // 如果是基本值类型,则返回的创建的'空对象'
  // 如果是引用类型,则返回这个引用类型的对象
  var flag = ret && ret instanceof Object
  return flag ? ret : obj
}
// test
function A(name) {
  this.name = name
}
var a1 = myNew(A, 'Lee')
var a2 = new A('Lee')
console.log(a1, a2)

手写一个 promise

const PENDING = 'pending'
const RESOLVED = 'resolved'
const REJECTED = 'rejected'

function MyPromise(fn) {
  // 保存初始化状态
  var self = this

  // 初始化状态
  this.state = PENDING

  // 用于保存 resolve 或者 rejected 传入的值
  this.value = null

  // 用于保存 resolve 的回调函数
  this.resolvedCallbacks = []

  // 用于保存 reject 的回调函数
  this.rejectedCallbacks = []

  // 状态转变为 resolved 方法
  function resolve(value) {
    // 判断传入元素是否为 Promise 值,如果是,则状态改变必须等待前一个状态改变后再进行改变
    if (value instanceof MyPromise) {
      return value.then(resolve, reject)
    }

    // 保证代码的执行顺序为本轮事件循环的末尾
    setTimeout(() => {
      // 只有状态为 pending 时才能转变,
      if (self.state === PENDING) {
        // 修改状态
        self.state = RESOLVED

        // 设置传入的值
        self.value = value

        // 执行回调函数
        self.resolvedCallbacks.forEach((callback) => {
          callback(value)
        })
      }
    }, 0)
  }

  // 状态转变为 rejected 方法
  function reject(value) {
    // 保证代码的执行顺序为本轮事件循环的末尾
    setTimeout(() => {
      // 只有状态为 pending 时才能转变
      if (self.state === PENDING) {
        // 修改状态
        self.state = REJECTED

        // 设置传入的值
        self.value = value

        // 执行回调函数
        self.rejectedCallbacks.forEach((callback) => {
          callback(value)
        })
      }
    }, 0)
  }

  // 将两个方法传入函数执行
  try {
    fn(resolve, reject)
  } catch (e) {
    // 遇到错误时,捕获错误,执行 reject 函数
    reject(e)
  }
}

MyPromise.prototype.then = function (onResolved, onRejected) {
  // 首先判断两个参数是否为函数类型,因为这两个参数是可选参数
  onResolved =
    typeof onResolved === 'function'
      ? onResolved
      : function (value) {
          return value
        }

  onRejected =
    typeof onRejected === 'function'
      ? onRejected
      : function (error) {
          throw error
        }

  // 如果是等待状态,则将函数加入对应列表中
  if (this.state === PENDING) {
    this.resolvedCallbacks.push(onResolved)
    this.rejectedCallbacks.push(onRejected)
  }

  // 如果状态已经凝固,则直接执行对应状态的函数

  if (this.state === RESOLVED) {
    onResolved(this.value)
  }

  if (this.state === REJECTED) {
    onRejected(this.value)
  }
}

手写Promise.all()

Promise.all = function(iterator) {
  if (!Array.isArray(iterator)) return;
  let count = 0;
  let res = [];
  return new Promise((resolve, reject) => {
    for(let item of iterator) {
      Promise.resolve(item)
      .then(data => {
        res[count++] = data;
        if (count === iterator.length) {
          resolve(res);
        }
      })
      .catch(e => {
        reject(e);
      })
    }
  })
}

// test
let p1 = Promise.resolve(3);
let p2 = 4;
let p3 = new Promise(resolve => {
  setTimeout(resolve, 100, 'lee')
  // setTimeout的第三个往后参数都是用来作为第一个参数也就是函数的参数,也就是其实是setTimeout(resolve('lee'), 100)
});
Promise.all([p1, p2, p3]).then(data => {
  console.log(data);
})

手写Promise.race()

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

let p1 = new Promise(resolve => {
  setTimeout(resolve, 105, 'p1 done')
})
let p2 = new Promise(resolve => {
  setTimeout(resolve, 100, 'p2 done')
})
Promise.race([p1, p2]).then(data => {
  console.log(data); // p2 done
})

手写一个 Ajax

AJAX 包括以下几个步骤

  1. 创建 XMLHttpRequest 对象,也就是创建一个异步调用对象
  2. 创建一个新的 HTTP 请求,并指定该 HTTP 请求的方法、URL 及验证信息
  3. 设置响应 HTTP 请求状态变化的函数
  4. 发送 HTTP 请求
  5. 获取异步调用返回的数据
  6. 使用 JavaScript 和 DOM 实现局部刷新

一般实现:

const SERVER_URL = '/server'

let xhr = new XMLHttpRequest()

// 创建 Http 请求 第三个参数为async,指定请求是否为异步方式,默认为 true。
xhr.open('GET', SERVER_URL, true)

// 设置请求头信息
xhr.responseType = 'json'
xhr.setRequestHeader('Accept', 'application/json')

// 设置状态监听函数
xhr.onreadystatechange = function () {
  if (this.readyState !== 4) return

  // 当请求成功时
  if (this.status === 200) {
    handle(this.responseText)
  } else {
    console.error(this.statusText)
  }
}

// 设置请求失败时的监听函数
xhr.onerror = function () {
  console.error(this.statusText)
}

// 发送 Http 请求
xhr.send(null)

promise 封装实现:

function getJSON(url) {
  // 返回一个 promise 对象
  return new Promise(function (resolve, reject) {
    let xhr = new XMLHttpRequest()

    // 新建一个 http 请求, 第三个参数为async,指定请求是否为异步方式,默认为 true。
    xhr.open('GET', url, true)

    // 设置状态的监听函数
    xhr.onreadystatechange = function () {
      /*0: 请求未初始化
        1: 服务器连接已建立
        2: 请求已接收
        3: 请求处理中
        4: 请求已完成,且响应已就绪*/
      if (this.readyState !== 4) return

      // 当请求成功或失败时,改变 promise 的状态
      /*200: "OK"
        404: 未找到页面*/
      if (this.status === 200) {
        resolve(this.responseText)
      } else {
        reject(new Error(this.statusText))
      }
    }

    // 设置响应的数据类型
    xhr.responseType = 'json'

    // 设置请求头信息
    xhr.setRequestHeader('Accept', 'application/json')

    // 发送 http 请求
    xhr.send(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();

斐波那契数列(递归,DP,循环)

  • 递归

时间复杂度O(2^N) 空间复杂度O(1)

function fiber(n) {
  if (n == 0 || n == 1) {
    return n
  } else {
    return fiber(n - 1) + fiber(n - 2)
  }
}

console.log(fiber(5))
  • DP 动态规划

时间复杂度O(N) 空间复杂度O(N)

function fiber(n) {
  if (n === 0 || n === 1) return n
  var dp = [0, 1]
  for (let i = 2; i <= n; i++) {
    dp[i] = dp[i - 1] + dp[i - 2]
  }
  return dp[n]
}
  • 循环

时间复杂夫O(N) 空间复杂度O(1)

function fiber(n) {
  if (n === 0 || n === 1) return n
  var ret = 1
  var a = 0
  var b = 1
  for (let i = 2; i < n; i++) {
    a = b
    b = ret
    ret = a + b
  }
  return ret
}

数组去重

  • 借助栈的方法
let quCh = arr => {
  let res = [];
  while(arr.length > 0) {
    let item = arr.shift();
    res.indexOf(item) === -1 && res.push(item);
  }
  return res;
}
console.log(quCh([1, 4, 5, 6, 6, 7, 7, 999, 999, 8, 7, 999]));
  • set 方法
function quCh(arr) {
  return [...new Set(arr)]
}

console.log(quCh([1, 2, 2, 3, 4, 4]))
  • indexOf() + lastIndexOf()方法
let quCh = arr => {
  let result = [];
  arr.forEach((item, index) => {
    if (arr.indexOf(item, index) === arr.lastIndexOf(item) && result.indexOf(item) === -1) {
      result.push(item)
    }
  })
  return result;
}

let arr = [1, 4, 5, 6, 6, 7, 7, 999, 999, 8, 7, 999];
console.log(quCh(arr))
  • 循环
function quCh(arr) {
  arr.sort((a, b) => a - b);
  let res = []
  for (let i = 0; i < arr.length; i++) {
    if (arr[i] !== arr[i - 1]) {
      res.push(arr[i])
    }
  }
  return res
}

console.log(quCh([1, 2, 2, 3, 3, 4, 3, 5, 5]))
  • includes()方法
function quCh(arr) {
  let res = []
  arr.forEach(item => {
    if (!res.includes(item)) {
      res.push(item)
    }
  })
  return res
}

console.log(quCh([1, 2, 3, 4, 3, 3, 3, 7, 6, 6, 9, 9]))

实现一个add函数 满足add(1,2,3)与add(1)(2)(3)结果相同

这是函数柯里化的一种基本表现形式

function add() {
  let args = [...arguments];
  let fn = function() {
    args.push(...arguments);
    return fn;
  }
  fn.toString = function(){ // toString
    return args.reduce((t,v)=>t+v);
  }
  return fn;
}

alert(add(1,2,3));
alert(add(1)(2)(3));

函数柯里化

参考链接

函数的柯里化:curry(又叫部分求值)

把接受多个参数的函数变成接受一个参数的函数,并返回一个新的函数;

实现方法:用一个闭包,返回一个函数,这个函数每次执行都会改写储存参数的数组,当函数的参数够了之后,便会执行

ES5 实现

function curry(fn, args = []) {
  // 获取函数需要的参数长度
  var length = fn.length
  return function () {
    // 拼接得到现有的所有参数
    for (let i = 0; i < arguments.length; i++) {
      args.push(arguments[i])
    }
    // 判断参数的长度是否已经满足函数所需参数的长度
    if (args.length >= length) {
      // 如果满足,执行函数
      return fn.apply(this, args)
    } else {
      // 如果不满足,递归返回科里化的函数,等待参数的传入
      return curry.call(this, 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))

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))

手写一个 flatten 函数(数组降维)

  • 方法 1:Array.prototype.flat()
var arr1 = [1, 2, [3, 4]]
arr1.flat()
// [1, 2, 3, 4]

var arr2 = [1, 2, [3, 4, [5, 6]]]
arr2.flat()
// [1, 2, 3, 4, [5, 6]]

var arr3 = [1, 2, [3, 4, [5, 6]]]
arr3.flat(2)
// [1, 2, 3, 4, 5, 6]

//使用 Infinity,可展开任意深度的嵌套数组
var arr4 = [1, 2, [3, 4, [5, 6, [7, 8, [9, 10]]]]]
arr4.flat(Infinity)
// [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
  • 方法 2:使用栈
// 无递归数组扁平化,使用堆栈
// 注意:深度的控制比较低效,因为需要检查每一个值的深度
// 也可能在 shift / unshift 上进行 w/o 反转,但是末端的数组 OPs 更快
function flatten(arr) {
  const stack = [...arr]
  const res = []
  while (stack.length) {
    // 使用 pop 从 stack 中取出并移除值
    const next = stack.pop()
    if (Array.isArray(next)) {
      // 使用 push 送回内层数组中的元素,不会改动原始输入
      stack.push(...next)
    } else {
      res.unshift(next)
    }
  }
  return res
}

//test
var arr1 = [1, 2, 3, [1, 2, 3, 4, [2, 3, 4]], [2, 3], 4, 5]
flatten(arr1) // [1, 2, 3, 1, 2, 3, 4, 2, 3, 4, 2, 3, 4, 5]
  • 方法 3: toString
function flatten(arr) {
    const res = arr.toString().split(',');
    // 这里要注意arr存在空数组的情况要踢掉
    return result = res.filter(item => item !== '').map(Number);
}
console.log(flatten([1,9,[2,3],[],[0,999,[66, [100,200]]]]))
  • 方法 4:reduce + concat + isArray + recursivity
// 使用 reduce、concat 和递归展开无限多层嵌套的数组
function flatDeep(arr, d = 1) {
  return d > 0
    ? arr.reduce(
        (acc, val) =>
          acc.concat(Array.isArray(val) ? flatDeep(val, d - 1) : val), [])
    : arr.slice()
}

// test
var arr1 = [1, 2, 3, [1, 2, 3, 4, [2, 3, 4]]]
flatDeep(arr1, Infinity)
// [1, 2, 3, 1, 2, 3, 4, 2, 3, 4]
  • 方法 5:forEach+isArray+push+recursivity
// forEach 遍历数组会自动跳过空元素
const eachFlat = (arr = [], depth = 1) => {
  const result = []; // 缓存递归结果
  // 开始递归
  (function flat(arr, depth) {
    // forEach 会自动去除数组空位
    arr.forEach((item) => {
      // 控制递归深度
      if (Array.isArray(item) && depth > 0) {
        // 递归数组
        flat(item, depth - 1)
      } else {
        // 缓存元素
        result.push(item)
      }
    })
  })(arr, depth)
  // 返回递归结果
  return result
}

// for of 循环不能去除数组空位,需要手动去除
const forFlat = (arr = [], depth = 1) => {
  const result = [];
  (function flat(arr, depth) {
    for (let item of arr) {
      if (Array.isArray(item) && depth > 0) {
        flat(item, depth - 1)
      } else {
        // 去除空元素,添加非undefined元素
        item !== void 0 && result.push(item)
      }
    }
  })(arr, depth)
  return result
}

深拷贝 浅拷贝

  • 浅拷贝

浅拷贝是指把一个对象obj的属性值直接拷贝给另一个对象,其中包括了原始类型的值,还有引用类型的内存地址

// 方法1:原生的Object.assign(target, src)方法来实现浅拷贝
let obj = {
  name: 'Lee',
  age: 18,
  gf: {
    name: 'Yjj',
    age: 18
  }
}
let newObj = Object.assign({}, obj);
obj.gf.age = 25;
console.log(newObj) // {"name":"lee","age":18,"gf":{"name":"yjj","age":25}}
// 注意这个方法target对象是第一个参数
// 方法2:扩展运算符(也是浅拷贝)
let obj1 = {
  name: 'Lee',
  age: 18
}
let newObj1 = {...obj}
obj1.age = 25;
console.log(newObj1); // {name: "Lee", age: 18}

// 对象里面有对象
let obj2 = {
  name: 'Lee',
  age: 18,
  gf: {
    name: 'Yjj',
    age: 18
  }
}
let newObj2 = {...obj2};
obj2.gf.age = 25;
console.log(newObj2); // {"name":"lee","age":18,"gf":{"name":"yjj","age":25}}
// 方法3:函数法
function shallowClone(obj) {
  if (!obj || typeof obj !== 'object') return
  let newObj = Array.isArray(obj) ? [] : {}
  for (let key in obj) {
    if (obj.hasOwnProperty(key)) {
      newObj[key] = obj[key]
    }
  }
  return newObj
}
  • 深拷贝

一行代码:

let newObj = JSON.parse(JSON.stringify(oldObj))

函数:

function deepClone(obj) {
  if (!obj || typeof obj !== 'object') return
  let newObj = Array.isArray(obj) ? [] : {}
  for (let key in obj) {
    if (obj.hasOwnProperty(key)) {
      newObj[key] =
        typeof obj[key] === 'object' ? deepClone(obj[key]) : obj[key]
    }
  }
  return newObj
}

// test
let obj = {
  age: 1,
  jobs: {
    first: 'FE',
  },
  schools: [
    {
      name: 'shenda',
    },
    {
      name: 'shiyan',
    },
  ],
  arr: [
    [
      {
        value: '1',
      },
    ],
    [
      {
        value: '2',
      },
    ],
  ],
}
console.log(deepClone(obj))

手写一个单例模式

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

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 关键字, 就表示该方法不会被实例继承, 而是直接通过类来调用, 这就称为“ 静态方法”。

手写一个观察者模式

var events = (function () {
  var topics = {}

  return {
    // 注册监听函数
    subscribe: function (topic, handler) {
      if (!topics.hasOwnProperty(topic)) {
        topics[topic] = []
      }
      topics[topic].push(handler)
    },

    // 发布事件,触发观察者回调事件
    publish: function (topic, info) {
      if (topics.hasOwnProperty(topic)) {
        topics[topic].forEach(function (handler) {
          handler(info)
        })
      }
    },

    // 移除主题的一个观察者的回调事件
    remove: function (topic, handler) {
      if (!topics.hasOwnProperty(topic)) return

      var handlerIndex = -1
      topics[topic].forEach(function (item, index) {
        if (item === handler) {
          handlerIndex = index
        }
      })

      if (handlerIndex >= 0) {
        topics[topic].splice(handlerIndex, 1)
      }
    },

    // 移除主题的所有观察者的回调事件
    removeAll: function (topic) {
      if (topics.hasOwnProperty(topic)) {
        topics[topic] = []
      }
    },
  }
})()

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
  • 来看一个简单的用途:
document.querySelector('###btn').addEventListener('click', function () {
  alert('You click this btn');
}, false)

我们平时对 DOM 的事件绑定就是一个非常典型的 发布-订阅者模式 ,这里我们需要监听用户点击按钮这个动作,但是我们却无法知道用户什么时候去点击,所以我们订阅 按钮上的 click 事件,只要按钮被点击时,那么按钮就会向订阅者发布这个消息,我们就可以做对应的操作了。

  • 再看vue中一个简答的用途:子组件与父组件通信

Vue 中我们通过 props 完成父组件向子组件传递数据,子组件与父组件通信我们通过自定义事件即 $on,$emit来实现,其实也就是通过 $emit 来发布消息,并对订阅者 $on 做统一处理 ~

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

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

获取一个页面所有标签的个数

function find() {
  var map = {};
  function __find(node) {
    if (node.nodeType === 1) {
      //这里我们用nodeName属性,直接获取节点的节点名称
      var tagName = node.nodeName;
      //判断对象中存在不存在同类的节点,若存在则添加,不存在则添加并赋值为1
      map[tagName] = map[tagName] ? map[tagName] + 1 : 1;
    }
    //获取该元素节点的所有子节点
    var children = node.childNodes;
    for (var i = 0; i < children.length; i++) {
      //递归调用
      __find(children[i])
    }
  }
  __find(document);
  return map;
}

异步输出结果

  • await 后面接一个会 return new promise 的函数并执行它
async function async1() {
  console.log('async1 start')
  await async2() // 会返回一个promise
  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('script end')
输出:
script start
async1 start
async2
promise1
script end
async1 end
promise2
setTimeout
  • process.nextTick()要优于promise.then执行

node中存在着一个特殊的队列,即nextTick queue。这个队列中的回调执行虽然没有被表示为一个阶段,当时这些事件却会在每一个阶段执行完毕准备进入下一个阶段时优先执行。当事件循环准备进入下一个阶段之前,会先检查nextTick queue中是否有任务,如果有,那么会先清空这个队列。

process.nextTick(function(){
    console.log(7);
});

new Promise(function(resolve){
    console.log(3);
    resolve();
    console.log(4);
}).then(function(){
    console.log(5);
});

process.nextTick(function(){
    console.log(8);
});
3
4
7
8
5
  • 以下两题来源于面试
async function f(){
    await new Promise(resolve => {
        setTimeout(() => {
            console.log('1');
        }, 2000);
    })
    await new Promise(resolve => {
        setTimeout(() => {
            console.log('2');
        }, 3000);
    })

    console.log('3');
}

f()
// 2秒后输出1,原因是await之后返回的promise状态没有发生变化,一直是pending
// 在promise中加上   resolve() 就能打印  3 了
async function f2() {
    let promiseA = new Promise(resolve => {
        setTimeout(() => {
            console.log('1');
        }, 2000);
    })

    let promiseB = new Promise(resolve => {
        setTimeout(() => {
            console.log('2');
        }, 3000);
    })

    await promiseA;
    await promiseB;
    console.log('3');
}
f2()
// 2秒打印1,再过1秒打印2
// 这里3不打印的原因也是因为await返回的promise状态没有变化,一直是pending
// 在promise中加上   resolve() 就能打印  3 了

水平垂直居中

  • html代码
<div class="out">
  <div class="inner"></div>
</div>

flex布局

.out {
  width: 400px;
  height: 400px;
  background-color: ###cccccc;
  display: flex;
  justify-content: center;
  align-items: center;
}

.inner {
  width: 200px;
  height: 200px;
  background-color: red;
}

position 定位

  • margin-top margin-left
.out {
  width: 400px;
  height: 400px;
  background-color: ###cccccc;
  position: relative;
}

.inner {
  width: 200px;
  height: 200px;
  background-color: red;
  position: absolute;
  top: 50%;
  left: 50%;
  /* 已知子盒子的宽高 */
  margin-top: -100px;
  margin-left: -100px;
}
  • calc()函数
.out {
  width: 400px;
  height: 400px;
  background-color: ###cccccc;
  position: relative;
}

.inner {
  width: 200px;
  height: 200px;
  background-color: red;
  position: absolute;
  /* css calc()函数 需要注意的是,运算符前后都需要保留一个空格 */
  left: calc(50% - 100px);
  top: calc(50% - 100px);
}
  • transform属性
.out {
  width: 400px;
  height: 400px;
  background-color: ###cccccc;
  position: relative;
}

.inner {
  width: 200px;
  height: 200px;
  background-color: red;
  position: absolute;
  top: 50%;
  left: 50%;
  /* css3 的transform属性 */
  transform: translate(-50%, -50%);
}

三角形 扇形

三角形

div {
  width: 0;
  height: 0;
  border: 100px solid transparent;
  border-left-color: red;
}

扇形

div {
  height: 0;
  width: 0;
  border: 100px solid transparent;
  border-radius: 50%;
  border-left-color: red;
}

实现圆形可点击区域

方法 1:

设置 div 的 border-radius:50%。

###circle {
 background: red;
 width: 100px;
 height: 100px;
 border-radius: 50%;
}

<div id="circle"></div>

方法 2:

<img>通过 usemap 映射到<map>的 circle 形<area>。

<img src="images/lanlvseImg.png" usemap="###Map" />
<map name="Map" id="Map">
  <area
    shape="circle"
    coords="100,100,50"
    href="http://www.baidu.com"
    rel="external nofollow"
    target="_blank"
  />
</map>

方法 3: JS 实现,获取鼠标点击位置坐标,判断其到圆点的距离是否不大于圆的半径,来判断点击位置是否在圆内。

document.onclick = function (e) {
  var r = 50
  var x1 = 100,
    y1 = 100 // 圆心坐标
  var x = e.clientX,
    y = e.clientY
  var distance = Math.abs(Math.sqrt(Math.pow(x - x1, 2) + Math.pow(y - y1, 2)))
  if (distance < r) {
    alert('OK')
  }
}

如何将浮点数点左边的数每三位添加一个逗号,如 12000000.11 转化为『12,000,000.11』

toLocaleString()

function format(number) {
  return number.toLocaleString();
}

replace

function format(number) {
  return number && number.replace(/(?!^)(?=(\d{3})+\.)/g, ",");
}
function format(number) {
  return number && number.toString().replace(/(\d)(?=(\d{3})+\.)/g, $2 => $2 + ',')
}