面试常考js源码实现

171 阅读8分钟

前言

本文只是对面试常用的源码实现进行了一次总结,提供了具体的实现方法,巩固下js知识,希望对你有所帮助。

1、实现一个简单的路由Router

  • ** hash 版 **
// html
<a href="/index">首页</a>
<a href="/about">关于</a>
<div id="content"></div>

<script>
  function Router() {
    // 存放路由
    this.routers = {}
    // 当前hash
    this.currentUrl = ''
    this.init()
  }
  Router.prototype = {
    // 定制路由规则
    route: function (path, callback) {
      this.routers[path] = callback || function () {}
    },
    // 刷新路由
    refresh: function () {
      this.currentUrl = location.hash.slice(1) || '/'
      this.routers[this.currentUrl] && this.routers[this.currentUrl]()
    },
    // 窗口监视
    init: function () {
      window.addEventListener('load', this.refresh.bind(this), false)
      window.addEventListener('hashchange', this.refresh.bind(this), false)
    }
  }

  // 调用
  var router = new Router()
  router.route('/index', function () {
    // todo
  })
  router.route('/about', function () {
     // todo
  })
</script>
  • ** history 版 **
 let aAll = document.getElementsByTagName('a'),
    content = document.getElementById('content');
  [].slice.call(aAll).forEach(item => {
    item.addEventListener('click', function (e) {
      // 阻止默认事件
      e.preventDefault()
      // 获取对应属性
      let href = e.target.getAttribute('href')
      // 添加一条记录,并保存到state中
      history.pushState({
        pathname: href
      }, '', href)
      // 根据href进行一些视图渲染
      content.innerHTML = `<div>${href.slice(1)}组件</div>`
    }, false)
  })
  // 监听popstate事件,一般浏览器前进后退时触发
  window.addEventListener('popstate', function (e) {
    // 根据e.state更新视图
    content.innerHTML = `<div>${e.state.pathname.slice(1)}组件</div>`
  }, false)

** 两者比较:**

-** hash模式 **

1、留下个#, 不够美观, 有代码洁癖的可以使用history模式,但这种模式兼容性较好

2、http请求不会携带#后的信息,改变hash也不会重新刷新页面

-** history模式 **

3、不能在本地直接打开文件使用,要在服务端环境下使用

4、利用了HTML5 History API拓展中新增的pushState() 和 replaceState() 方法。 pushState() 会向历史记录中添加一条记录,replaceState()对历史记录进行修改,虽然改变了当前的url,但浏览器不会立即向后端发送请求

5、可以自由的修改path, 但服务器中没有相应的响应或者资源的时候,返回返回一个404

2、手写一个call与apply

  • ** call **
Function.prototype.myCall = function (context) {
  if (typeof this !== 'function') {
    throw new TypeError('error')
  }
  // 如果没有指向window
  context = context || window
  // 将调用call函数的对象添加到context的属性中
  context.fn = this
  // 获取参数
  const args = [...arguments].slice(1)
  // 执行该属性
  const result = context.fn(...args)
  // 删除该属性
  delete context.fn
  // 返回结果
  return result
}

 // 调用
 let call0 = function() {
 	console.log(this.name, arguments)
 }
 
 call0.prototype.name = 'jack';
 
 let call1 = {
 	name: 'liLei'
 }
 
 call0.myCall(call1, 1, 2, 3) // liLei [1, 2, 3]
  • ** apply **
Function.prototype.myApply = function (context) {
  if (typeof this !== 'function') {
    throw new TypeError('error')
  }
  // 如果没有指向window
  context = context || window
  // 将调用call函数的对象添加到context的属性中
  context.fn = this
  // 获取参数
  const args = arguments[1]
  // 执行该属性
  const result = context.fn(...args)
  // 删除该属性
  delete context.fn
  // 返回结果
  return result
}

 // 调用
 let apply0 = function() {
 	console.log(this.name, arguments)
 }
 
 a.prototype.name = 'jack';
 
 let apply1 = {
 	name: 'liLei'
 }
 
 apply0.myApply(apply1, [1, 2, 3]) // liLei [1, 2, 3]

call与apply 只有参数的区别,第一个参数相同,call()后面是一串参数列表,apply()只有两个参数,第二个参数是个数组

3、手写一个bind

Function.prototype.myBind = function (context) {
  if (typeof this !== 'function') {
    throw new TypeError('error')
  }
  // 保存this
  let _this = this 
  // 获取参数
  const args = Array.prototype.slice.call(arguments, 1)
  // 返回一个函数
  return function Fn() {
    if (this instanceof Fn) {
      return new _this(...arg, ...arguments)
    } else {
      return _this.apply(context, arg.concat(...arguments))
    }
  }
}

 // 调用
 let bind0 = function() {
 	console.log(this.name, arguments)
 }
 
 bind0.prototype.name = 'bar';
 
 let bind1 = {
 	name: 'foo'
 }
 
bind0.myBind(bind1, 1, 2, 3)() // foo [1, 2, 3]

call、aplly、bind核心都是改变this的指向,不同点call、aplly是直接调用了函数,而bind 是返回一个新的函数, 需要手动去调用

4、实现new操作

要实现new操作,我们要知道通过new关键字来调用构造函数经历了哪些

1、创建一个新的空对象

2、对象的 __ proto__ 属性指向构造函数的prototype指向的对象

3、已新建的对象为上下文,执行构造函数中代码为这个对象添加属性

4、如果构造函数有返回值且是对象,则返回该对象。否则返回新建的对象

function myNew() {
  // 创建一个新的对象
  let newObj = {}
  // 获取外部传入的构造器
  let _constructor = Array.prototype.shift.call(arguments)
  // 新的对象隐式原型指向构造器的显示原型
  newObj.__proto__ = _constructor.prototype
  // 调用构造器, 改变其this指向
  let result = _constructor.apply(newObj, arguments)
  // 如果构造函数返回值是对象则返回这个对象,如果不是对象则返回新的对象
   if (result && typeof result === 'function' || typeof result === 'object') {
        return result;
    }
    return newObj
}

// 调用
 new0 = function(name) {
  this.name = name;
};

let newObj = myNew(new0, 'foo');

console.log(newObj); // { name: "foo" }
console.log(newObj instanceof new0); // true

5、实现instanceof

// 原理:left的__proto__是不是等于right.prototype,如果不等于再找
// left.__proto__.__proto__ 直到__proto__为null为止

function myInstanceof(left, right) {
  let leftVal = left.__proto__
  let rightVal = right.prototype
  while (true) {
    if (leftVal === null) return false
    if (leftVal === rightVal) return true
    leftVal = leftVal.__proto__
  }
}

// 测试
function Person() {}

let foo = new Person()

console.log(myInstanceof(foo, Person)) // true

6、Object.create原理

// 将传入的对象作为原型
function myCreate(obj) {
  function Fn() {}
  Fn.prototype = obj
  return new Fn()
}

7、浅拷贝

浅拷贝只复制地址值,实际上还是指向同一堆内存中的数据

// 1、 ...实现
let obj0 = {...{a: 1, b: 2}}  // {a: 1, b: 2}

这里需要注意...只能对第一层进行深拷贝,再多一层就无法深拷贝了,所以还是属于浅拷贝

let obj1 = {a: 1, b: { c: 3 } }
let obj2 = {...obj1 } 
console.log(obj2.b.c = 10) // 10
console.log(obj1.b.c) // 10

上述栗子可以看出obj2修改c的值时候,原来的obj1中的c也影响了

// 2、 Object.assign()
let bar = Object.assign({}, {a: 1, b: 2}) // {a: 1, b: 2}

8、深拷贝

深拷贝则是重新创建了一个相同的数据,二者指向的堆内存的地址值是不同的。这个时候修改赋值前的变量数据不会影响赋值后的变量

// 1、简单的函数递归拷贝
function deepCopy(obj) {
 let copy = obj instanceof Array ? [] : {}
  for (let item in obj) {
    if (obj.hasOwnProperty(item)) {
      copy[item] = typeof obj[item] === 'object' ? deepClone(obj[item]) : obj[item]
    }
  }
  return copy
}

// 测试
let arr = [{
    id: 1004,
    name: "jack",
    age: '22'
  },
  {
    id: 1002,
    name: "alice",
    age: '2162'
  },

  {
    id: 1003,
    name: "haha",
    age: '2232'
  }
]
let result = deepCopy(arr)
console.log(result[0].name = 'jason') // jason
console.log(arr[0].name) // jack

这里没有

// 2、JSON.parse / JSON.stringify
let obj = {name: 'foo', age: 20}
let copy = JSON.parse(JSON.stringify(obj)) // {name: 'foo', age: 20}

JSON.parse / JSON.stringify 这种方法也是有弊端的,例如拷贝的数据比较大,会比较耗时,如果obj里有函数,undefined,则序列化的结果会把函数或 undefined丢失;如果obj里有RegExp、Error对象,则序列化的结果将只得到空对象,如果obj里有NaN、Infinity和-Infinity,则序列化的结果会变成null等,这里不做展开了,详情可以看看这篇文章 blog.csdn.net/u013565133/…

9、实现一个简单的双向绑定

  • ** Object.defineProperty()版 **
// html
<input placeholder="请输入" id="input"></input>
<span id="span"></span>

<script>
	const data = {
      text: 'this is text'
    }
    let input = document.getElementById('input'),
      span = document.getElementById('span')
    Object.defineProperty(data, 'text', {
      set(Val) {
        input.value = Val
        span.innerHTML = Val
      },
      get() {
        console.log('数据更新了')
      }
    })
    // 触发视图更新
    input.addEventListener('keyup', function (e) {
      proxy.text = e.target.value
    })
</script>
  • ** Proxy()版 **
// html
<input placeholder="请输入" id="input"></input>
<span id="span"></span>

<script>
	const data = {
      text: 'this is text'
    }
    let input = document.getElementById('input'),
      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
    })
</script>

** 两者比较 **

  • Object.defineProperty主要问题在于,无法监听数组变化,准确的说是不支持数组的API,而proxy可以支持数组的API

  • Object.defineProperty的作用是劫持对象的属性,劫持的为getter和setter方法,是在对象属性变化时执行操作,proxy是劫持整个对象

  • Proxy会返回一个代理对象,我们只需要操作新对象即可,而Object.defineProperty只能遍历对象属性直接修改

  • Proxy有13种方法可以调用, 比Object.defineProperty要更加丰富的多

  • Object.defineProperty的兼容性要比Proxy要好

10、实现一个数组flat

const arr = [7, 2, 8, 4, [5, [{
      name: 'jason',
      age: 20
    }, 10, [{
      test: 'string'
    }]], ]]

// 这里使用reduce()来实现
const flat = arr => {
  return arr.reduce((pre, cur) => {
    return pre.concat(Array.isArray(cur) ? flat(cur) : cur);
  }, []);
}

flat(arr)  // [7, 2, 8, 4, 5, {name: 'jason', age: 20 }, 10, { test:'string' }]

11、实现一个简单的Promise

// 初始myPromise
function myPromise(constructor) {
  let _this = this
  // 定义初始状态
  _this.status = 'pending'
  // 定义resolved状态
  _this.value = undefined
  // 定义rejected状态
  _this.reason = undefined

  // resolved回调收集
  _this.onFullfilledArr = []
  // reject回调收集
  _this.onRejectedArr = []

  // resolve方法
  function resolve(value) {
    // 保证状态不可以逆转
    if (_this.status === 'pending') {
      _this.value = value
      _this.status = 'resolved'
      _this.onFullfilledArr.forEach(function (e) {
        e(_this.value)
      })
    }
  }

  // reject方法
  function reject(reason) {
    // 保证状态不可以逆转
    if (_this.status === 'pending') {
      _this.reason = reason
      _this.status = 'rejected'
      _this.onRejectedArr.forEach(function (e) {
        e(_this.reason)
      })
    }
  }

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

// 添加then 方法
myPromise.prototype.then = function (onFullfilled, onRejected) {
  let _this = this,
    _promise
  switch (_this.status) {
    case "pending":
      _promise = new myPromise(function (resolve, reject) {
        _this.onFullfilledArr.push(function () {
          try {
            let temple = onFullfilled(_this.value)
            resolve(temple)
          } catch (e) {
            reject(e)
          }
        })
        _this.onRejectedArr.push(function () {
          try {
            let temple = onRejected(_this.reason)
            resolve(temple)
          } catch (e) {
            reject(e)
          }
        })
      })
      break;
    case 'resolved':
      _promise = new myPromise(function (resolve, reject) {
        try {
          let temple = onFullfilled(_this.value)
          resolve(temple)
        } catch (e) {
          reject(e)
        }
      })
      break;
    case "rejected":
      _promise = new myPromise(function (resolve, reject) {
        try {
          resolve(onRejected(_this.reason))
        } catch (e) {
          reject(e)
        }
      })
      break;
    default:
  }
  return _promise
}

// 测试
let p = new myPromise(function (resolve, reject) {
  setTimeout(function () {
    resolve(10)
  }, 1000)
})
p.then(function (value) {
  console.log(value)       // 10
}).then(function () {
  console.log('链式调用1')  // 链式调用1
}).then(function () {
  console.log('链式调用2')  // 链式调用2
})

12、 数组去重

let arr = [1, 2, 3, 3, 6, 7, 2, 5, 6, 7, 1, 10]

// 1. new Set()
let result = [...new Set(arr)]

// 2、indexOf
function duplicate(array) {
  let arr = []
  for (const item of array) {
    if (arr.indexOf(item) === -1) {
      arr.push(item)
    }
  }
  return arr
}

// 3、splice
function duplicate(array) {
  let len = array.length
  for (var i = 0; i < len; i++) {
    for (var j = i + 1; j < len; j++) {
      if (array[i] === array[j]) {
        array.splice(j, 1)
        j--;
      }
    }
  }
  return array
}

// 4、filter
function duplicate(array) {
  return array.filter((item, index) => {
    return array.indexOf(item) === index
  })
}

// 5、hasOwnProperty
function duplicate(array) {
    var obj = {};
    return array.filter((item, index, arr) => {
        return obj.hasOwnProperty(typeof item + item) ? false : (obj[typeof item + item] = true)
    })
}

13、实现一个防抖与节流

  • ** 节流 **:是函数不管被触发多少次,都会在规定的时间内去执行一次处理函数
// 这种是利用时间戳,在规定的时间内触发
function throttle(fn, delay) {
  // 获取触发时间戳
  let prev = Date.now()
  return function () {
    // 保存
    let context = this
    // 获取参数
    let args = arguments
    // 获取当前时间戳
    let now = Date.now()
    // 对比时间
    if (now - prev >= delay) {
      fn.apply(context, args)
      // 重置触发时间
      prev = Date.now()
    }
  }
}

// 这个是定时器触发,在设定时间内触发
fucntion throttle(fn, delay) {
  // 设置初始状态
  let timer = null
  return funtion() {
  	// 保存
    let context = this;
    // 获取参数
    let args = arguments;
    if (!timer) {
      // 赋值定时器
      timer = setTimeout(function () {
        // 改变指向
        fn.apply(context, args);
        // 清空
        timer = null;
      }, delay);
    }
  }
}
  • ** 防抖 **: 在一定时间内多次触发事件,只会执行一次处理函数,如果在规定时间内再次触发,先清除之前的定时器,重新计时
function debounce (fn, delay) {
  // 设置初始状态
  let timer = null
  return function () {
    let context = this
    let args = arguments
    // 如果在规定时间内再次触发,先清除定时器
    if(timer) clearTimeout(timer)
    timer = setTimeout(function () {
      fn.apply(context, args)
    }, delay)
  }
}

14、实现一个柯里化函数

function myCurry() {
  // 获取参数,转成数组
  // 这里还是可以这样转换
  // 1、Array.prototype.slice.call(arguments)
  // 2、[].slice.call(arguments)
  // 3、Array.from(arguments)
  let args = [...arguments];
  // 收集所有参数
  let addfun = function () {
    args.push(...arguments);
    return addfun;
  }
  // 利用toString隐式转换的特性,返回最终值
  addfun.toString = function () {
    return args.reduce((a, b) => {
      return a + b;
    });
  }
  return addfun;
}

// 调用
myCurry(1, 2, 3) // 6
myCurry(1, 2, 3)(4) //10

最后

如有问题,欢迎指出,后续会持续更新!!!