JavaScript手写题

93 阅读9分钟

一、实现Object.create()

理解

  • 将传入对象作为新建对象的原型
let obj = {}
let newObj = Object.create(obj)
console.log(newObj.__proto__ == obj); // true

实现

  • 创建一个构造函数 让构造函数的原型指向传入的对象
  • 返回构造函数的实例 f
  • f.__proto__ == F.prototype == obj
function create(obj) {
  function F() {}
  F.prototype = obj
  F.prototype.constructor = F
  return new F()
}
let obj = {}
let newObj = create(obj)
console.log(newObj.__proto__ == obj) // true

二、实现instanceof

理解

  • instanceof 可以判断引用数据类型 但是不能判断基本数据类型
  • 本质是在判断在对象的原型链中能否找到对应类型的原型
  • Array.prototype.__proto__ == Object.prototype
  • Function.Prototype.__proto__ == Object.prototype
let a = {}
let b = 123
let c = function() {}
let d = []
console.log(a instanceof Object)    // true
console.log(b instanceof Number)    // false
console.log(c instanceof Function)  // true
console.loh(d instanceof Array)     // true
console.log(c instanceof Object)    // true
console.loh(d instanceof Object)    // true

实现

function myInstanceof(left, right) {
  if(typeof left !== 'object' && typeof left !== 'function' || left === null) return false
  let proto = Object.getPrototypeOf(left)
  let prototype = right.prototype
  while(true) {
    if(!proto) return false
    if(proto === prototype) return true
    proto = Object.getPrototypeOf(proto)
  }
}
let a = {}
let b = 123
let c = function() {}
let d = []
console.log(myInstanceof(a, Object))    // true
console.log(myInstanceof(b, Number))    // false
console.log(myInstanceof(c, Object))    // true
console.log(myInstanceof(d, Object))    // true

三、实现new操作符

  1. 创建一个新对象,这个对象的__proto__要指向构造函数的原型对象
  2. 执行构造函数
  3. 返回值为object类型则作为new方法的返回值返回,否则返回上述全新对象
function _new() {
  const [constructor, ...args] = [...arguments]
  if(typeof constructor != 'function') return
  let obj = {}
  obj.__proto__ = constructor.prototype
  let result = constructor.apply(obj, args)
  if(result && (typeof result == 'object' || typeof result == 'function')) {
    return result
  }
  return obj
}

四、实现防抖函数

  1. 在事件被触发n秒后再执行回调,如果在这n秒内又被触发,则重新计时
  2. 应用场景:浏览器窗口大小resize避免次数过于频繁。登录等按钮避免发送多次请求
function debounce(fn, delay) {
  let timer = null
  return function(...args) {
    let context = this
    if(timer) clearTimeout(timer)
    timer = setTimeout(() => {
      fn.apply(context, args)
    }, delay)
  }
}

五、实现节流函数

  1. 规定在一个单位时间内,只能触发一次函数。如果这个单位时间内触发多次函数,只有一次生效
  2. 节流重在加锁 flag = false
function throttle(fn, delay) {
  let flag = true
  return function(...args) {
    let context = this
    if(flag) {
      flag = false
      setTimeout(() => {
        flag = true
        fn.apply(context, args)
      }, delay)
    }
  }
}

六、判断类型函数

  1. 如果为null 就返回'null'
  2. 如果为引用数据类型 就通过Object.prototype.toString.call方法判断
  3. 基本数据类型就通过typeof判断
function getType(value) {
  if(value === null) return 'null'
  if(typeof value === 'object') {
    let valueClass = Object.prototype.toString.call(value)
    let type = valueClass.split(' ')[1].split('')
    type.pop()
    return type.join('').toLowerCase()
  }
  return typeof value
}

七、手写call函数

Function.prototype.mycall = function() {
  let [context, ...args] = [...arguments]
  if(typeof this !== 'function') return
  context = context || window
  context.fn = this
  let result = context.fn(...args)
  delete context.fn
  return result
}

let obj = {
  a: 1
}
function output(b,c) {
  console.log(this.a,b,c);
}
output.mycall(obj,2,3) // 1 2 3

八、手写apply函数

Function.prototype.myapply = function() {
  if(typeof this !== 'function') return
  let context = arguments[0] || window
  context.fn = this
  let result = null
  if(arguments[1]) {
    result = context.fn(arguments[1])
  } else {
    result = context.fn()
  }
  delete context.fn
  return result
}

let obj = {
  a: 1
}
function output(arr) {
  console.log(this.a, arr);
}
output.myapply(obj,[2,3]) // 1 [ 2, 3 ]

九、手写bind函数

  1. bind的普通实现
Function.prototype.mybind = function(context, ...args){
  return (...newArgs) => {
      return this.call(context,...args, ...newArgs)
  }
}

let obj = {
  z: 3
}
function fn(x, y) {
  console.log(x, y, this.z);
}

var returnFunc = fn.mybind(obj, 1)
returnFunc(2) // 1 2 3
  1. newthis的优先级高于bind,所以需要区分调用bind返回函数时是普通调用还是new调用
Function.prototype.mybind = function(context) {
  if(typeof this !== 'function') return
  var args = [...arguments].slice(1), fn = this
  return function Fn() {
    return fn.apply(
      this instanceof Fn ? this : context, // new对this的的优先级更高
      args.concat(...arguments)
    )
  }
}

let obj = {
  z: 3
}
function fn(x, y, z) {
  this.x = x
  this.y = y
  z && (this.z = z)
  console.log(this.x, this.y, this.z);
}
fn.prototype.age = 25

var returnFunc = fn.mybind(obj, 1)
var person = new returnFunc(2, 4) // 1 2 4
// 因为new对this的优先级更高

console.log(person.age) // undefined
// 无法调用原型属性
  1. new调用bind返回函数,并可以使用原型上的方法和属性。借助一个空的构造函数F实现。
Function.prototype.mybind = function(context) {
  if(typeof this !== 'function') return
  var args = [...arguments].slice(1), fn = this
  var returnFunc = function () {
    return fn.apply(
      this instanceof returnFunc ? this : context,
      args.concat(...arguments)
    )
  }
  var F = function() {}
  F.prototype = this.prototype
  returnFunc.prototype = new F()
  return returnFunc
}

let obj = {}
function fn() {}
fn.prototype.age = 26

var returnFunc = fn.mybind(obj)
var person = new returnFunc()
console.log(person.age) // 26

十、函数柯里化

  1. 简单实现一个add的柯里化
function add() {
  let args = [...arguments]

  let inner = function() {
    args.push(...arguments)
    return inner
  }
  inner.toString = function() {
    return args.reduce((prev, curt) => prev + curt)
  }
  return inner
}
console.log(add(1)(2)(3)(4) + '') // 10
  1. 已知道需要进行柯里化的addSum函数的入参个数是确定的,3
// 方法一
let currying = (fn, ...args) =>
            fn.length > args.length ?
            (...arguments) => currying(fn, ...args, ...arguments) :
            fn(...args)

let addSum  = (a, b ,c) => a + b + c
let add = currying(addSum)
console.log(add(1)(2)(3)) // 6

// 方法二
function currying(fn, ...args) {
  let allArgs = [...args]
  return function inner() {
    allArgs = [...allArgs, ...arguments]
    if(fn.length > allArgs.length) {
      return inner
    } else {
      return fn(...allArgs)
    }
  }
}
let addSum  = (a, b ,c) => a + b + c // 10
let add = currying(addSum)
console.log(add(1)(2)(3))
  1. 已知道需要进行柯里化的addSum函数的入参个数是不确定的
function currying(fn, ...args) {
  let allArgs = [...args]
  return function inner() {
    if(arguments.length) {
      allArgs = [...allArgs, ...arguments]
      return inner
    } else {
      return fn(...allArgs)
    }
  }
}
let addSum  = (...args) => args.reduce((prev, curt) => prev + curt)
let add = currying(addSum)
console.log(add(1)(2)(3)()) // 6

十一、浅拷贝

  • Object.assign()
    • Object.assign(target,source1,source2,....)
  • 扩展运算符
    • let obj2 = {...obj}
  • Array.prototype.slice
    • arr.slice()
  • Array.prototype.concat
    • arr.concat()
  • 代码实现浅拷贝
function shallowCopy(obj) {
  if(typeof obj !== 'object' || !obj) return
  let newObj = Array.isArray(obj) ? [] : {}
  for(let key in obj) {
    if(obj.hasOwnProperty(key)) {
      newObj[key] = obj[key]
    }
  }
  return newObj
}
let obj = { a: 1, b: { c: 2 } }
let newObj = shallowCopy(obj)
obj.a = 2
obj.b.c = 1
console.log(obj)    // { a: 2, b: { c: 1 } }
console.log(newObj) // { a: 1, b: { c: 1 } }

十二、深拷贝

function deepCopy(obj) {
  if(typeof obj !== 'object' || !obj) return
  let newObj = Array.isArray(obj) ? [] : {}
  for(let key in obj) {
    if(obj.hasOwnProperty(key)) {
      newObj[key] = typeof obj[key] === 'object' ? deepCopy(obj[key]) : obj[key]
    }
  }
  return newObj
}
let obj = { a: 1, b: { c: 2 } }
let newObj = deepCopy(obj)
obj.a = 2
obj.b.c = 1
console.log(obj)    // { a: 2, b: { c: 1 } }
console.log(newObj) // { a: 1, b: { c: 2 } }

十三、实现简单的AJAX

  1. 原始版
const SERVER_URL = '/server'
const  xml = new XMLHttpRequest()
xml.open('GET', SERVER_URL, true) // open(方法,路径,是否异步加载,用户授权)
xml.onreadystatechange = function() {
  if(this.readyState !== 4) return
  if(this.status === 200) {
    handle(this.response);
  } else {
    console.error(this.statusText);
  }
}
xml.onerror = function() {
  console.error(this.statusText);
}
xml.setRequestHeader('Accept', 'application/json')
xml.responseType = 'json'
xml.send()
  1. promise封装版
function AJAX(url) {
  return new Promise((resolve, reject) => {
    const  xml = new XMLHttpRequest()
    xml.open('GET', url, true) // open(方法,路径,是否异步加载,用户授权)
    xml.onreadystatechange = function() {
      if(this.readyState !== 4) return
      if(this.status === 200) {
        resolve(this.response);
      } else {
        reject(this.statusText);
      }
    }
    xml.onerror = function() {
      reject(this.statusText);
    }
    xml.setRequestHeader('Accept', 'application/json')
    xml.responseType = 'json'
    xml.send()
  })
}

十四、控制并发

  1. 每完成一个请求就会有一个新的请求加入,保证请求的最大数为maxCount
function limitRunTask(tasks, n) {
  return new Promise((resolve, reject) => {
    let index = 0, finish = 0, start = 0, res = [];
    function run() {
      if (finish == tasks.length) {
        resolve(res)
        return
      }
      while (start < n && index < tasks.length) {
        start++
        let cur = index
        tasks[index++].then(v => {
          res[cur] = v        
        }).finally(() => {
          start--
          finish++
          run()
        })
      }
    }
    run()
  })
}
function fetch(url) {
  return new Promise((resolve,reject) => {
    resolve(url)
  })
}
const urlList = []
for(let i  = 0; i < 100; i++) {
  urlList.push('url' + i)
}
const promises = urlList.map(item => fetch(item))
const maxCount = 10
limitRunTask(promises, maxCount).then(res => {
  console.log(res)
})
  1. 将数组分成多个数目相等的小数组,每次最多只开启maxConcurrentNum个并发请求,以此来控制并发数量。每当一组请求完成后再发送新的一批请求
const urls = []
for(let i  = 0; i < 100; i++) {
  urls.push('url' + i)
}
const maxConcurrentNum = 10; // 最大并发数 
// 数组分块,chunk表示每批次数量,返回数组二维数组 
function chunk(arr, chunk) { 
  let result = []; 
  for (let i = 0, len = arr.length; i < len; i += chunk) { 
    result.push(arr.slice(i, i + chunk)); 
   } 
   return result; 
 }
// 异步请求方法 
function fetchUrl(url) { 
  return new Promise((resolve, reject) => { 
    setTimeout(() => {
      resolve(url)
    })
   })
}
// 对url数组进行分块处理
const chunkedUrls = chunk(urls, maxConcurrentNum);
(async function () {
  try {
    for (let urls of chunkedUrls) {
      const promises = urls.map(url => fetchUrl(url));
      // 等待所有promises完成执行,并将结果存入results数组中
      const results = await Promise.all(promises);
      console.log('results:', results);
    }
  } catch (err) {
   console.error(err);
  }
})()

十五、实现一个 sleep 函数

function sleep(delay) {
  let start = (new Date()).getTime()
  while((new Date()).getTime() - start < delay) continue
}
function test() {
  console.log('111');
  sleep(2000);
  console.log('222');
}

test()

十六、实现(5).add(3).sub(2)

Number.prototype.add = function(value) {
  if(typeof value != 'number') throw new Error('请输入数字~')
  return this.valueOf() + value
}
Number.prototype.sub = function(value) {
  if(typeof value != 'number') throw new Error('请输入数字~')
  return this.valueOf() - value
}
console.log((5).add(3).sub(2))

十七、实现一个字符串匹配算法,从长度为 slen 的字符串 S 中,查找长度为 tlen 的字符串 T,若存在返回所在位置

function getStrIndex(S, T) {
  let slen = S.length
  let tlen = T.length
  if(!slen || !tlen || slen < tlen) return -1
  for(let i = 0; i < slen; i++) {
    let j = 0,k = i
    if(S[k] == T[j]) {
      k++
      j++
      while(j < tlen && k < slen) {
        if(S[k] != T[j]) break
        else {
          j++
          k++
        }
      }
      if(j == tlen) return i
    }
  }
  return -1
}

console.log(getStrIndex("Hello World", "rl")) // 8

十八、使用递归实现累加

function add(x, y) {
  if(x === y) return x
  else {
    return y + add(x, y - 1)
  }
}
console.log(add(1,100)) // 5050

十九、async await的实现

const getData = () => new Promise(resolve => setTimeout(() => resolve("data"), 1000))

function asyncToGenerator(generatorFunc) {
  return function() {
    const gen = generatorFunc.apply(this, arguments)
    return new Promise((resolve, reject) => {
      function step(key, arg) {
        let generatorResult
        try {
          generatorResult = gen[key](arg)
        } catch (error) {
          return reject(error)
        }
        const { value, done } = generatorResult
        if (done) {
          return resolve(value)
        } else {
          return Promise.resolve(value).then(val => step('next', val), err => step('throw', err))
        }
      }
      step("next")
    })
  }
}
  
var test = asyncToGenerator(
  function* testG() {
    // await被编译成了yield
    const data = yield getData()
    console.log('data: ', data);
    const data2 = yield getData()
    console.log('data2: ', data2);
    return 'success'
  }
)

test().then(res => console.log(res))

参考地址:手写async await的最简实现(20行) - 掘金 (juejin.cn)

二十、实现一个模块 math ,支持链式调用

class Math {
  constructor(value) {
      let hasInitValue = true;
      if (value === undefined) {
          value = NaN;
          hasInitValue = false;
      }
      Object.defineProperties(this, {
          value: {
              enumerable: true,
              value: value,
          },
          hasInitValue: {
              enumerable: false,
              value: hasInitValue,
          },
      });
  }

  add(...args) {
      const init = this.hasInitValue ? this.value : args.shift();
      const value = args.reduce((pv, cv) => pv + cv, init);
      return new Math(value);
  }

  minus(...args) {
      const init = this.hasInitValue ? this.value : args.shift();
      const value = args.reduce((pv, cv) => pv - cv, init);
      return new Math(value);
  }

  times(...args) {
      const init = this.hasInitValue ? this.value : args.shift();
      const value = args.reduce((pv, cv) => pv * cv, init);
      return new Math(value);
  }

  divide(...args) {
      const init = this.hasInitValue ? this.value : args.shift();
      const value = args.reduce((pv, cv) => pv / cv, init);
      return new Math(value);
  }

  toJSON() {
      return this.valueOf();
  }

  toString() {
      return String(this.valueOf());
  }

  valueOf() {
      return this.value;
  }
}

let math = new Math()
console.log(math.add(2,4).minus(3).times(2)) // Math { value: 6 }

21. 给定一个字符串,求不含有重复字符的最长子串的长度

var lengthOfLongestSubstring = function (s) {
  var temp = [];
  var maxs = 0;
  if (s == "") {
      return 0;
  }
  if (s.length == 1) {
      return 1;
  }
  let i = 0
  while (i < s.length) {
    if(!temp.includes(s[i])) {
      temp.push(s[i])
     maxs++
    }
    i++
  }
  return maxs;
}

console.log(lengthOfLongestSubstring('abcabcbb')); // 3
console.log(lengthOfLongestSubstring('bbbbb')); // 1
console.log(lengthOfLongestSubstring('pwwkewrxyz')); // 8

22. 用ES6proxy实现 arr[-1] 的访问

function proxyArray(arr) {
  const length = arr.length
  return new Proxy(arr, {
    get(target, key) {
      key = +key // 将string转换为int
      while(key < 0) {
        key += length
      }
      return target[key]
    }
  })
}
let a = proxyArray([1, 2, 3, 4, 5, 6, 7, 8, 9]);
console.log(a[1]);    // 2
console.log(a[-10]);  // 9
console.log(a[-20]);  // 8

23. 将数组扁平转换为树结构

递归方式

const flatArr = [
  { id: '01', parentId: 0, name: '节点1' },
  { id: '011', parentId: '01', name: '节点1-1' },
  { id: '0111', parentId: '011', name: '节点1-1-1' },
  { id: '02', parentId: 0, name: '节点2' },
  { id: '022', parentId: '02', name: '节点2-2' },
  { id: '023', parentId: '02', name: '节点2-3' },
  { id: '0222', parentId: '022', name: '节点2-2-2' },
  { id: '03', parentId: 0, name: '节点3' },
]

function getTreeData (arr, parentId) {
  function loop(parentId) {
    return arr.reduce((pre,cur) => {
      if(cur.parentId == parentId) {
        cur.children = loop(cur.id)
        pre.push(cur)
      }
      return pre
    }, [])
  }
  return loop(parentId)
}

let result = getTreeData(flatArr, 0)
console.log(result);

非递归方式

const flatArr = [
  { id: '01', parentId: 0, name: '节点1' },
  { id: '011', parentId: '01', name: '节点1-1' },
  { id: '0111', parentId: '011', name: '节点1-1-1' },
  { id: '02', parentId: 0, name: '节点2' },
  { id: '022', parentId: '02', name: '节点2-2' },
  { id: '023', parentId: '02', name: '节点2-3' },
  { id: '0222', parentId: '022', name: '节点2-2-2' },
  { id: '03', parentId: 0, name: '节点3' },
]

function getTreeData (arr) {
  let data = arr.filter(item => {
    item.children = arr.filter(e => {
      return item.id == e.parentId
    })
    return !item.parentId
  })
  return data
}

let result = getTreeData(flatArr)
console.log(result);

24. 去除字符串中出现次数最少的字符,不改变原字符串的顺序

function f(str){
  let obj = {};
  for(let i=0;i<str.length;i++){
      if (obj[str[i]]) {
          obj[str[i]] ++;
      } else {
          obj[str[i]] = 1;
      }
  }
  let countArr = [];
  Object.keys(obj).forEach((item)=>{
      countArr.push(obj[item]);
  });
  let min = Math.min(...countArr);
  for(let key in obj) {
    if(obj[key] == min) {
      str = str.replaceAll(key, '')
    }
  }
  return str
}
let str = 'aaabbbcceeff'
let result = f(str)
console.log(result); // aaabbb