前言
梳理总结一遍面试比较经常碰到的 JavaScript 手撕题,方便面试的时候复习,同时也巩固一下 JS 的基本功。
数据类型判断
function type_of(obj) {
return Object.prototype.toString.call(obj).slice(8, -1)
}
console.log('数据类型判断 :>> ', typeof1([])); // Array
作用域安全的构造函数
直接看代码:
function Person(name, age) {
if (this instanceof Person) {
this.name = name
this.age = age
} else {
return new Person(name, age)
}
}
上面例子中,Person 构造函数使用 this 给属性赋值。当和 new 关键字连用时会创建一个新的对象,问题在当没有使用 new 关键字的情况下,this 会映射到全局对象 window 上,导致错误对象属性增加。
实现setInterval
原生的setInterval会出现两个问题:
- 某些间隔会被跳过
- 多个定时器的代码执行之间的间隔可能会比预期的小。
直接看代码:
setTimeout(function() {
// 处理逻辑
setTimeout(arguments.callee, wait)
}, wait)
上述模式链式调用了 setTimeout,每次函数执行的时候会创建一个新的定时器,内部的 setTimeout 调用使用 arguments.callee 来获取对当前执行函数的引用,并为其设置另外一个定时器。在前一个定时器代码执行完之前,不会向队列插入新的定时器代码,确保不会缺失时间间隔。同时,也确保了下一次定时器执行之前,至少要等待指定的间隔,避免了连续的运行。
惰性函数
所谓惰性载入,指函数执行的分支只会发生一次。一般因为各浏览器之间的行为差异,经常会在函数中包含了大量的 if 语句,以检查浏览器特性,解决不同浏览器的兼容问题。
方式一:
function createXHR() {
if (typeof XMLHttpRequest !== 'undefined) {
createXHR = function() {...}
} else {
createXHR = function() {...}
}
return createXHR()
}
方式二:
const createXHR = (function() {
if (typeof XMLHttpRequest !== 'undefined) {
return createXHR = function() {...}
} else {
return createXHR = function() {...}
}
})
继承
原型链继承会出现子类在实例化的时候不能给父类构造函数传参;原型中包含的引用类型属性将被所有实例共享;借用构造器继承会出现子类不能继承父类原型上属性;组合式继承则会两次调用构造函数。最好的继承方式是寄生组合式继承。ES6 关键字extends 底层实现也是采用寄生组合式继承。所以这边只实现寄生组合继承,代码如下:
function People(name) {
this.name = name
}
function ChinesePeople(name, age) {
this.age = age
People.call(this, name)
}
function _extends(parent, son) {
function Middle() {
this.constructor = son
}
Middle.prototype = parent.prototype
let middle = new Middle()
return middle
}
ChinesePeople.prototype = _extends(People, ChinesePeople)
let chinesePeople = new ChinesePeople('lisi', 18)
console.log('寄生组合式继承:', chinesePeople);
// 寄生组合式继承: ChinesePeople { age: 18, name: 'lisi' }
数组去重
ES5 实现:
function unique(arr) {
let res = []
for(let i = 0; i< arr.length; i++) {
if (res.indexOf(arr[i]) === -1) {
res.push(arr[i])
}
}
return res
}
ES6 实现:
const unique1 = (arr) => [...new Set(arr)]
console.log('数组去重 :>> ', unique1([undefined,undefined,1,2,2,2,3,4,5]));
// 数组去重 :>> [ undefined, 1, 2, 3, 4, 5 ]
数组扁平化
ES5 实现:
function flatten(arr) {
let res = []
for(let i = 0; i< arr.length; i++) {
if (Array.isArray(arr[i])) {
res = res.concat(flatten(arr[i]))
} else {
res.push(arr[i])
}
}
return res
}
console.log('数组扁平化 :>> ', flatten([1, [2,3]])); // [1,2,3]
ES6 实现:
function flatten(arr) {
while (arr.some(item => Array.isArray(item))) {
arr = [].concat(...arr);
}
return arr;
}
浅拷贝
只拷贝第一层对象,深层次的属性改变,拷贝对象也会收到影响。
function shallowCopy(obj) {
if (typeof obj !== 'object') return obj
let newObj = obj instanceof Array ? [] : {}
for(let key in obj) {
if (obj.hasOwnProperty(key)) {
newObj[key] = obj[key]
}
}
return newObj
}
let a = {
a: 1,
b: {b:2},
c: [3],
d: Date
}
let b = shallowCopy(a)
b.b.b = 100
console.log('浅拷贝 :>> ', b, a);
// 浅拷贝 :>> { a: 1, b: { b: 100 }, c: [ 3 ], d: [Function: Date] }
// { a: 1, b: { b: 100 }, c: [ 3 ], d: [Function: Date] }
深拷贝
每个对象属性都用新的变量空间存储,改变属性值不会相互影响。
function deepClone (target, map = new Map()) {
let constructor = target.constructor
if (/^RegExp|Date$/i.test(constructor.name)) {
return new constructor(target)
}
if (target && typeof target === 'object') {
let clone = Array.isArray(target) ? [] : {}
if (map.get(target)) {
return map.get(target)
}
map.set(target, clone)
for (let key in target) {
clone[key] = deepClone(target[key], map)
}
return map.get(target)
} else {
return target
}
}
let a1 = {
a: 1,
b: {b:2},
c: Symbol(3),
d: new Date()
}
let b1 = deepClone(a1)
b1.b.b = 100
console.log('深拷贝 :>> ', b1, a1);
// 深拷贝 :>> { a: 1, b: { b: 100 }, c: Symbol(3), d: 2021-12-06T09:10:20.403Z }
// { a: 1, b: { b: 2 }, c: Symbol(3), d: 2021-12-06T09:10:20.403Z }
函数防抖
触发高频事件 N 秒后只会执行一次,如果 N 秒内事件再次触发,则会重新计时。
function debounce(fn, wait) {
let timeoutId
return function() {
if (timeoutId) {
const args = arguments
const context = this
clearTimeout(timeoutId)
timeoutId = setTimeout(function(){
fn.apply(context, args)
}, wait)
}
}
}
函数节流
触发高频事件,且 N 秒内只执行一次。
function throttle(fn, wait) {
let timer
return function() {
if (timer) return
const args = arguments
const context = this
timer = setTimeout(function() {
fn.aplly(context, args)
timer = null
}, wait)
}
}
实现instanceof
function instance_of(L, R) {
let O = R.prototype
L = L.__proto__
while(true) {
if (L === null) return false
if (L === O) return true
L = L.__proto__
}
}
console.log('实现instanceof :>> ', instance_of([], String));
// 实现instanceof :>> false
实现new
function objectFactory() {
const obj = new Object()
const constructor = [].shift.call(arguments)
obj.__proto__ = constructor.prototype
const ret = constructor.apply(obj, arguments)
return typeof ret === 'object' ? ret : obj
}
实现call
call做了什么:
- 将函数设为对象的属性
- 执行&删除这个函数
- 指定this到函数并传入给定参数执行函数
- 如果不传入参数,默认指向为 window
Function.prototype.myCall = function(context) {
var context = context || window
context.fn = this
const args = Array.prototype.slice.call(arguments, 1);
let result = context.fn(...args)
delete context.fn
return result
}
function bar(name, age) {
return {
name,
age
}
}
console.log('模拟call', bar.myCall(this, 'lisi', 18));
// 模拟call { name: 'lisi', age: 18 }
实现apply
跟call实现同理。
Function.prototype.myApply = function(context, arr) {
var context = context || window
context.fn = this
let result
if (!arr) {
result = context.fn()
} else {
result = context.fn(...arr)
}
delete context.fn
return result
}
function bar(name, age) {
return {
name,
age
}
}
console.log('模拟apply', bar.myApply(this, ['lisi',18]));
// 模拟apply { name: 'lisi', age: 18 }
实现bind
简易版本实现:
Function.prototype.bind1 = function(context, ...args) {
var context = context || window
return (...newargs) => {
return this.myCall(context, ...args, ...newargs)
}
}
let binds = bar.bind(this, 'zhangsan', 20)
console.log('模拟bind :>> ', binds());
// 模拟bind :>> { name: 'zhangsan', age: 20 }
柯里化函数
柯里化就是把接受多个参数的函数变换成接受一个单一参数的函数,并且返回接受余下参数返回结果的一种应用。
function curry(fn, ...args) {
return fn.length > args.length
? (...arguments) => curry(fn, ...args, ...arguments)
: fn(...args)
}
let addSum = (a, b, c) => a+b+c
let add = curry(addSum)
console.log('柯里化函数',add(1)(2)(3), add(1, 2)(3), add(1,2,3))
// 柯里化函数 6 6 6
偏函数
什么是偏函数?偏函数就是将一个 n 参的函数转换成固定 x 参的函数,剩余参数(n - x)将在下次调用全部传入。
function partial(fn, ...args) {
return (...newargs) => {
return fn(...args, ...newargs)
}
}
let partialAdd = partial(add, 1)
console.log('偏函数 :>> ', partialAdd(2, 3));
// 偏函数 :>> 6
实现sleep
某个时间后就去执行某个函数,使用Promise封装。
function sleep (fn, wait) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(fn)
}, wait)
})
}
// let saySomething = (name) => console.log(`hello,${name}`)
// async function autoPlay() {
// let demo = await sleep(saySomething('张三'),1000)
// let demo2 = await sleep(saySomething('李四'),1000)
// let demo3 = await sleep(saySomething('xxs'),1000)
// }
// autoPlay()
手写ajax
const getJSON = function(url) {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open('GET', url, false);
xhr.setRequestHeader('Accept', 'application/json');
xhr.onreadystatechange = function() {
if (xhr.readyState !== 4) return;
if (xhr.status === 200 || xhr.status === 304) {
resolve(xhr.responseText);
} else {
reject(new Error(xhr.responseText));
}
}
xhr.send();
})
}
实现trim
去掉首位多余的空格。
String.prototype.trim = function(){
return this.replace(/^\s+|\s+$/g, '')
}
//或者
function trim(string){
return string.replace(/^\s+|\s+$/g, '')
}
console.log('trim :>> ', trim(' ssposc '));
// ssposc
实现数组reduce
简易版本实现:
Array.prototype.myReduce = function(fn, initVal) {
let result = initVal, i = 0
if (typeof initVal === 'undefined') {
result = this[i]
i++
}
while(i < this.length) {
result = fn(result, this[i])
i++
}
return result
}
const result = [1,2,3].myReduce((a, b) => a + b
, 1)
console.log('reduce :>> ', result);
实现Object.create
function create(proto) {
function Fn() {};
Fn.prototype = proto;
Fn.prototype.constructor = Fn;
return new Fn();
}
let demo = {
c : '123'
}
let cc = Object.create(demo)
JSONP
JSONP 核心原理:script 标签不受同源策略约束,所以可以用来进行跨域请求,优点是兼容性好,但是只能用于 GET 请求;
const jsonp = ({ url, params, callbackName }) => {
const generateUrl = () => {
let dataSrc = ''
for (let key in params) {
if (params.hasOwnProperty(key)) {
dataSrc += `${key}=${params[key]}&`
}
}
dataSrc += `callback=${callbackName}`
return `${url}?${dataSrc}`
}
return new Promise((resolve, reject) => {
const scriptEle = document.createElement('script')
scriptEle.src = generateUrl()
document.body.appendChild(scriptEle)
window[callbackName] = data => {
resolve(data)
document.removeChild(scriptEle)
}
})
}
事件总线
class EventEmitter {
constructor() {
this.cache = {}
}
on(name, fn) {
if (this.cache[name]) {
this.cache[name].push(fn)
} else {
this.cache[name] = [fn]
}
}
off(name, fn) {
let tasks = this.cache[name]
if (tasks) {
const index = tasks.findIndex(f => f === fn || f.callback === fn)
if (index >= 0) {
tasks.splice(index, 1)
}
}
}
emit(name, once = false, ...args) {
if (this.cache[name]) {
// 创建副本,如果回调函数内继续注册相同事件,会造成死循环
let tasks = this.cache[name].slice()
for (let fn of tasks) {
fn(...args)
}
if (once) {
delete this.cache[name]
}
}
}
}
// 测试
let eventBus = new EventEmitter()
let fn1 = function(name, age) {
console.log(`${name} ${age}`)
}
let fn2 = function(name, age) {
console.log(`hello, ${name} ${age}`)
}
eventBus.on('aaa', fn1)
eventBus.on('aaa', fn2)
eventBus.emit('aaa', false, 'lisi', 20)
字符串模板
function render(template, data) {
const reg = /\{\{(\w+)\}\}/; // 模板字符串正则
if (reg.test(template)) { // 判断模板里是否有模板字符串
const name = reg.exec(template)[1]; // 查找当前模板里第一个模板字符串的字段
template = template.replace(reg, data[name]); // 将第一个模板字符串渲染
return render(template, data); // 递归的渲染并返回渲染后的结构
}
return template; // 如果模板没有模板字符串直接返回
}
测试:
let template = '我是{{name}},年龄{{age}},性别{{sex}}';
let person = { name: 'lisi', age: 20 } render(template, person);
// 我是lisi,年龄20,性别undefined
实现快速排序
function sort(arr) {
if (!arr.length) return
quickSort(arr, 0, arr.length - 1)
}
function quickSort(arr, start, end) {
if (start >= end) return
let left = start, right = end
let pivot = arr[Math.floor((left + end) / 2)]
while (left <= right) {
while (left <= right && arr[left] < pivot) {
left++
}
while (left <= right && arr[right] > pivot) {
right--
}
if (left <= right) {
let temp = arr[left]
arr[left] = arr[right]
arr[right] = temp
right--
left++
}
}
quickSort(arr, start, right)
quickSort(arr, left, end)
}
实现冒泡排序
function sortarr(arr) {
for (let i = 0; i < arr.length - 1; i++) {
for (let j = 0; j < arr.length - i - 1; j++) {
if (arr[j] > arr[j + 1]) {
let temp = arr[j]
arr[j] = arr[j + 1]
arr[j + 1] = temp
}
}
}
return arr
}