- 1. 为数字添加千位分隔符
- 2. 单例模式
- 3. 观察者模式
- 4. 发布订阅模式
- 5. 函数柯里化
- 6. 实现一个add方法,使结果满足如下预期
- 7. 实现 call、apply 方法
- 8. 实现 bind 方法
- 9. 实现 instanceof 方法
- 10. new 的本质
- 11. Object.create 的基本实现原理
- 12. 实现防抖和节流
- 13. 用 for 和 reduce 实现 map 和 filter
- 14. 使用 for 循环打印1-10,每个数字间隔 100ms
- 15. 使用 setTimeout 模拟 setInterval
- 16. ES5 实现继承
- 17. 实现 compose
- 18. 实现深拷贝
- 20. 数组扁平化
- 21. 对象扁平化
- 22. 图片懒加载
- 23. 手动编写一个ajax,不依赖第三方库
- 24. 实现
a == 1 && a == 2 && a == 3
为true
# 前端面试手写代码
# 1. 为数字添加千位分隔符
function milliFormat(num) {
return num && num.toString()
.replace(/\d+/, function(s){
return s.replace(/(\d)(?=(\d{3})+$)/g, '$1,')
})
}
toLocalString
方法也可以实现千位分隔符
**x(?=y)
匹配'x'
仅仅当x
后面跟着'y'**
**(?:x)
仅分组,不记录匹配项**
-$1,$2
就是按顺序对应小括号里面的正则 捕获到的内容。
# 2. 单例模式
class Single{
constructor(...args) {
console.log(...args)
}
}
Single.getInstance = (function() {
let instance
return function(...args) {
if (!instance) {
instance = new Single(...args)
}
return instance
}
})()
// 测试
const a = Single.getInstance('yang', 18)
const b = Single.getInstance('mike', 'male')
a === b //true
1. getInstance是类的静态方法,不能使用new SingleObject()
2. 创建出来的实例全等
# 3. 观察者模式
// 目标对象
class Subject{
constructor() {
this.subs = []
}
addSub = (sub) => {
this.subs.push(sub)
}
notify = (...args) => {
this.subs.map(sub => {
sub.update(...args)
})
}
}
// 观察者
class Observer{
update(...args) {
console.log(...args)
}
}
// 测试
const subject = new Subject()
const observer = new Observer()
subject.addSub(observer)
subject.notify('a', 'b', 'c')
首先是目标的构造函数,他有个数组,用于添加观察者。
还有个广播方法notify,遍历观察者数组后调用观察者们的update方法。
# 4. 发布订阅模式
class EventEmitter{
constructor() {
this._eventpool = {}
}
on(event, cb) {
this._eventpool[event]
? this._eventpool[event].push(cb)
: this._eventpool[event] = [cb]
}
emit(event, ...args) {
if (this._eventpool[event]) {
this._eventpool[event].map(cb => {
cb(...args)
})
}
}
off(event) {
delete this._eventpool[event]
}
once(event, cb) {
this.on(event, (...args) => {
cb(...args)
this.off(event)
})
}
}
// 测试
const emitter = new EventEmitter()
const cb = (...args) => {
console.log(...args)
}
emitter.on('a', cb)
emitter.emit('a', 'y', 'g', 'w')
emitter.emit('a', 'y', 'g', 'w')
emitter.once('b', cb)
emitter.emit('b', 'y', 'g', 'w')
emitter.emit('b', 'y', 'g', 'w')
基于一个主题/事件通道,订阅者subscriber通过自定义事件订阅主题,发布者publisher通过发布主题事件的方式发布。
观察者模式和发布订阅模式区别:
1. 在观察者模式中,观察者需要直接订阅目标事件。在目标发出内容改变的事件后,直接接收事件并作出响应。
2. 发布订阅模式相比观察者模式多了个主题/事件通道,订阅者和发布者不是直接关联的。
3. 观察者模式两个对象之间有很强的依赖关系;发布/订阅模式两个对象之间的耦合度低。
# 5. 函数柯里化
当我们没有重新定义 toString 与 valueOf 时,函数的隐式转换会调用默认的 toString 方法,它会将函数的定义内容作为字符串返回。
而当我们主动定义了 toString/vauleOf 方法时,那么隐式转换的返回结果则由我们自己控制了。其中 valueOf的优先级会比 toString 高一点。
柯里化好处:参数复用、延迟运行(返回函数,想什么时候运行什么时候运行)
function currying() {
const [fn, ...args] = [...arguments]
const cb = function() {
if (!arguments.length) {
return fn.apply(this, args)
}
args.push(...arguments)
return cb
}
// 视情况添加
cb.toString = () => {
return fn.apply(this, args)
}
return cb
}
function add() {
return [...arguments].reduce((sum, item) => sum + item, 0)
}
// 测试柯里化
const a = currying(add, 12, 24, 36)
a()
a(123)(123)(123)
# 6. 实现一个add方法,使结果满足如下预期
add(1)(2)(3) = 6;
add(1, 2, 3)(4) = 10;
add(1)(2)(3)(4)(5) = 15;
function add() {
const args = [...arguments]
const cb = function() {
args.push(...arguments)
return cb
}
cb.toString = () => {
return args.reduce((sum, item)=> sum + item)
}
return cb
}
# 7. 实现 call、apply 方法
Function.prototype.myCall = function() {
let [context = window, ...args] = [...arguments]
// 关键步骤,在 context 上调用方法,触发 this 绑定为 context,使用 Symbol 防止原有属性的覆盖
const key = Symbol('key')
context[key] = this
const res = context[key](...args)
delete context[key]
return res
}
Function.prototype.myApply = function() {
// 和call的不同点在于没有...,因为只有两个参数(context 和 数组)
let [context = window, args = []] = [...arguments]
// 关键步骤,在 context 上调用方法,触发 this 绑定为 context,使用 Symbol 防止原有属性的覆盖
const key = Symbol('key')
context[key] = this
let res = context[key](...args);
delete context[key]
return res
}
# 8. 实现 bind 方法
Function.prototype.myBind = function() {
const [context, ...args] = [...arguments]
const fn = this
const newFn = function() {
const newArgs = args.concat(...arguments)
if (this instanceof newFn) {
return fn.apply(this, newArgs)
}
return fn.apply(context, newArgs)
}
newFn.prototype = Object.create(fn.prototype)
return newFn
}
测试:
const obj = {
name: 'yang',
getName() {
console.log(this.name)
}
}
obj.getName()
const obj2 = {
name: 'mike'
}
const getName2 = obj.getName.bind(obj2)
getName2()
测试构造函数:
function Test(name) {
this.name = name
}
const newTest = Test.myBind (obj)
const obj = {
name: 'yang'
}
const instance = new Test('haha')
const instance2 = new newTest('hehe')
console.log(instance, instance2)
# 9. 实现 instanceof 方法
instanceof
运算符用来验证,一个对象是否为指定的构造函数的实例。obj instanceof Object
返回true
,就表示obj
对象是Object
的实例。
instanceof
的原理是检查右边构造函数的prototype
属性,是否在左边对象的原型链上。
// 思路:右边变量的原型存在于左边变量的原型链中。
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__
}
}
# 10. new 的本质
1. 创建一个新对象且将其隐式原型指向构造函数原型
2. 执行构造函数
3. 返回该对象
function myNew() {
const [fn, ...args] = [...arguments]
const obj = {
__proto__: fn.prototype
}
fn.call(obj, ...args)
return obj
}
# 11. Object.create 的基本实现原理
1. 创建一个空的构造函数
2. 将传入的对象作为其原型
3. 用该构造函数创建新的对象,并返回
function myCreate(proto) {
function F(){}
F.prototype = proto
return new F()
}
生成实例对象的常用方法是,使用new
命令让构造函数返回一个实例。但是很多时候,只能拿到一个实例对象,它可能根本不是由构建函数生成的,那么能不能从一个实例对象,生成另一个实例对象呢?
JavaScript 提供了Object.create
方法,用来满足这种需求。该方法接受一个对象作为参数,然后以它为原型,返回一个实例对象。该实例完全继承原型对象的属性。
// 原型对象
var A = {
print: function () {
console.log('hello');
}
};
// 实例对象
var B = Object.create(A);
Object.getPrototypeOf(B) === A // true
B.print() // hello
B.print === A.print // true
上面代码中,Object.create
方法以A
对象为原型,生成了B
对象。B
继承了A
的所有属性和方法。
# 12. 实现防抖和节流
所谓防抖,就是指触发事件后在 n 秒内函数只能执行一次,如果在 n 秒内又触发了事件,则会重新计算函数执行时间。
所谓节流,就是指连续触发事件但是在 n 秒中只执行一次函数。
区别:
(1) 函数节流不管事件触发有多频繁,都会保证在规定时间内一定会执行一次真正的事件处理函数,
而函数防抖只是在最后一次事件后才触发一次函数。
(2) 比如在页面的无限加载场景下,我们需要用户在滚动页面时,每隔一段时间发一次 Ajax 请求,
而不是在用户停下滚动页面操作时才去请求数据。这样的场景,就适合用节流技术来实现。
应用: 进行窗口的resize、scroll,输入框内容校验或请求ajax时
function debounce(fn, time) {
let timer
return function() {
const _this = this
const args = [...arguments]
if(timer) clearTimeout(timer)
timer = setTimeout(() => {
fn.apply(_this, args)
}, time)
}
}
function throttle(fn, time) {
let timer
return function() {
const _this = this
const args = [...arguments]
if (!timer) {
setTimeout(() => {
timer = null // 注意,一定要先置为null,再执行fn
fn.apply(_this, args)
}, time)
}
}
}
// 测试
function count() {
console.log('xxxxx')
}
window.onscroll = debounce(count, 500)
window.onscroll = throttle(count, 500)
# 13. 用 for 和 reduce 实现 map 和 filter
// for 实现 map
Array.prototype.myMap = function() {
const arr = this
const [fn, thisArg] = [...arguments]
const res = []
for (let i = 0; i < arr.length; i++) {
res.push(fn.call(thisArg, arr[i], i, arr))
}
return res
}
// for 实现 filter
Array.prototype.myFilter = function() {
const arr = this
const [fn, thisArg] = [...arguments]
const res = []
for (let i = 0; i < arr.length; i++) {
// 与上面的唯一不同
if (fn.call(thisArg, arr[i], i, arr)) {
res.push(arr[i])
}
}
return res
}
// reduce 实现 map
Array.prototype.myMap = function() {
const arr = this
const [fn, thisArg] = [...arguments]
return arr.reduce((acc, item, index) => {
acc.push(fn.call(thisArg, item, index, arr))
return acc
}, [])
}
// reduce 实现 filter
Array.prototype.myFilter = function() {
const arr = this
const [fn, thisArg] = [...arguments]
return arr.reduce((acc, item, index) => {
if (fn.call(thisArg, item, index, arr)) {
acc.push(item)
}
return acc
}, [])
}
# 14. 使用 for 循环打印1-10,每个数字间隔 100ms
// 使用 let
for (let i = 1; i < 11; i++) {
setTimeout(() => {
console.log(i)
}, 500 * i)
}
/**
* 使用闭包
* 整个 for 循环内部是一个闭包,注意将i传进去
*/
for (var i = 1; i < 11; i++) {
(function(i){
setTimeout(()=> {
console.log(i)
}, 500 * i)
})(i)
}
// 错误做法
for (var i = 0; i < 11; i++) {
setTimeout(() => {
console.log(i)
}, 500 * i)
}
# 15. 使用 setTimeout 模拟 setInterval
function mySetInterval(fn, delay) {
let timer;
const interval = () => {
fn();
timer = setTimeout(interval, delay);
};
setTimeout(interval, delay);
return {
cancel: () => {
clearTimeout(timer);
},
};
}
// 测试
const { cancel } = mySetInterval(() => console.log(888), 1000);
setTimeout(() => {
cancel();
}, 4000);
setInterval
有两个缺点:
- 使用
setInterval
时,某些间隔会被跳过 - 可能多个定时器会连续执行
setInterval 的回调函数并不是到时后立即执行,而是等系统计算资源空闲下来后才会执行。而下一次触发时间则是在 setInterval 回调函数执行完毕之后才开始计时,所以如果 setInterval 内执行的计算过于耗时,或者有其他耗时任务在执行,setInterval 的计时会越来越不准,延迟很厉害。
var startTime = new Date().getTime();
var count = 0;
//耗时任务
setInterval(function(){
var i = 0;
while(i++ < 100000000);
}, 0);
setInterval(function(){
count++;
console.log(new Date().getTime() - (startTime + count * 1000));
}, 1000);
代码里输出了setInterval触发时间和应该正确触发时间的延迟毫秒数
176
340
495
652
807
961
1114
1268
1425
1579
1734
1888
2048
2201
2357
2521
2679
2834
2996
......
setTimeout
模拟setInterval
好处:
- 在前一个定时器执行完前,不会向队列插入新的定时器(解决缺点一)
- 保证定时器间隔(解决缺点二)
参考资料:
# 16. ES5 实现继承
function Parent() {
}
function Child() {
Parent.call(this)
}
Child.prototype = Object.create(Parent.prototype)
Child.prototype.constructor = Child
这种方法称为寄生组合式继承。
- 寄生式继承的思路:在创建对象的函数中直接吸收其他对象的功能,然后对其进行扩展并返回。
- 构造函数的目的是为了复制属性,
Parent.call(this, name)
肯定不能少 Child.prototype =new Parent()
的目的是为了获取到父类原型对象(prototype
)上的方法,基于这个目的,有没有别的方法可以做到 在不需要实例化父类构造函数的情况下,也能得到父类原型对象上的方法呢? 当然可以,我们可以采用寄生式继承来得到父类原型对象上的方法。- 那么使用
Object.create
的原型链是什么样的呢?其实很简单:SubType.prototype.__ proto __ = SuperType.protype
也就是说,子类的原型相当于是父类原型的一个实例,这不就是实现了两者的链接了吗?
其他方式的不足
function Parent() {
this.name = 'parent'
}
Parent.prototype.say = function() {}
function Child() {
Parent.call(this)
this.type = 'child'
}
console.log(new Child().say()) // 报错
构造函数中的继承:
- 只用
Parent.call(this)
的缺点是,原型链上的属性并没被继承。
function Parent() {
this.name = 'parent'
this.play = [1, 2, 3]
}
function Child() {
this.type = 'child'
}
Child.prototype = new Parent()
const s1 = new Child()
const s2 = new Child()
console.log(s1.play, s2.play) // [1, 2, 3] [1, 2, 3]
s1.play.push(4)
console.log(s1.play, s2.play) // [1, 2, 3, 4] [1, 2, 3, 4]
单纯的原型链继承:
- 使用
Child.prototype = new Parent()
的缺点是,子类实例的引用类型是公用的,修改一个实例会引起另一个实例的改变。
function Parent(){
console.log('____')
this.play = [1, 2, 3]
}
function Child() {
Parent.call(this)
}
Child.prototype = new Parent()
const s1 = new Child()
const s2 = new Child()
console.log(s1.play, s2.play)
s1.play.push(4)
console.log(s1.play, s2.play)
简单的组合方式是,上述两种的相加。缺点是父级构造函数执行了两次。
function Parent() {
}
function Child() {
Parent.call(this)
}
Child.prototype = Parent.prototype
const s = new Child()
console.log(s.constructor) // Parent
Child.prototype = Parent.prototype
的缺点是:
- 无法区分一个实例是哪个原型对象创建的。若强行使用过
Child.prototype.constructor=Child
,会使得父类对象的contructor
属性也是Child
,显然错误。 - 并且,当我们想要在子对象原型中扩展一些属性以便之后继续继承的话,父对象的原型也会被改写,因为这里的原型对象实例始终只有一个,这也是这种继承方式的缺点。
# 17. 实现 compose
/**
* 实现以下功能:compose([a, b, c])('参数') => a(b(c('参数')))
*/
function compose(funcs) {
return funcs.reduce((a, b) => (...args) => a(b(...args)))
}
// 另一种
function compose(funcs) {
let index = funcs.length - 1
return function() {
let res = funcs[index].call(null, ...arguments)
while (--index >= 0) {
res = funcs[index].call(null, res)
}
return res
}
}
// 测试
const a = x => x * 2
const b = x => x + 5
const c = x => x * 3
compose([a, b, c])(123)
# 18. 实现深拷贝
1. 使用`JSON.stringify`和`JSON.parse`实现深拷贝:`JSON.stringify`把对象转成字符
串,再用`JSON.parse`把字符串转成新的对象;
缺陷:它会抛弃对象的`constructor`,深拷贝之后,不管这个对象原来的构造函数是什么,
在深拷贝之后都会变成`Object`;这种方法能正确处理的对象只有 `Number`, `String`,
`Boolean`, `Array`, 扁平对象,也就是说,只有可以转成`JSON`格式的对象才可以这样
用,像`function`没办法转成`JSON`;
2. `slice`是否为深拷贝
`slice()`和`concat()`都并非深拷贝,而是只拷贝第一层。
function deepCopy(obj, cache = new WeakMap()) {
if (!(obj instanceof Object)) return obj
// 防止循环引用
if (cache.get(obj)) return cache.get(obj)
// 支持函数
if (obj instanceof Function) {
// return new Function('return '+obj.toString())()
// 错误,对于`say(){}`这样的函数会报错
return eval(obj)
}
// 支持日期
if (obj instanceof Date) return new Date(obj)
// 支持正则对象
if (obj instanceof RegExp) return new RegExp(obj.source, obj.flags)
// 数组是 key 为数字素银的特殊对象
const res = Array.isArray(obj) ? [] : {}
// 缓存 copy 的对象,用于处理循环引用的情况
cache.set(obj, res)
Object.keys(obj).forEach((key) => {
if (obj[key] instanceof Object) {
res[key] = deepCopy(obj[key], cache)
} else {
res[key] = obj[key]
}
});
return res
}
// 测试
const source = {
name: 'Jack',
meta: {
age: 12,
birth: new Date('1997-10-10'),
ary: [1, 2, { a: 1 }],
say() {
console.log('Hello');
}
}
}
source.source = source
const newObj = deepCopy(source)
console.log(newObj.meta.ary[2] === source.meta.ary[2]); // false
console.log(newObj.meta.birth === source.meta.birth); // false
参考资料:
# 20. 数组扁平化
function recursionFlat(arr) {
const res = [];
arr.map((item) => {
if (Array.isArray(item)) {
res.push(...recursionFlat(item));
} else {
res.push(item);
}
});
return res;
}
function reduceFlat(arr) {
return arr.reduce(
(acc, item) => res.concat(Array.isArray(item) ? reduceFlat(item) : item),
[]
);
}
const source = [1, 2, [3, 4, [5, 6]], "7"];
console.log(recursionFlat(arr));
console.log(reduceFlat(arr));
# 21. 对象扁平化
function objectFlat(obj = {}) {
const res = {};
_objectFlat(obj, res);
return res;
}
function _objectFlat(item, res, preKey = "") {
Object.entries(item).forEach(([key, val]) => {
const newKey = preKey ? `${preKey}.${key}` : key;
if (val && typeof val === "object") {
_objectFlat(val, res, newKey);
} else {
res[newKey] = val;
}
});
}
// 测试
const source = { a: { b: { c: 1, d: 2 }, e: 3 }, f: { g: 2 } };
console.log(objectFlat(source));
# 22. 图片懒加载
// <img src="default.png" data-src="https://xxxx/real.png">
function isVisible(el) {
const position = el.getBoundingClientRect()
const windowHeight = document.documentElement.clientHeight
// 顶部边缘可见
const topVisible = position.top > 0 && position.top < windowHeight;
// 底部边缘可见
const bottomVisible = position.bottom < windowHeight && position.bottom > 0;
return topVisible || bottomVisible;
}
function imageLazyLoad() {
const images = document.querySelectorAll('img')
for (let img of images) {
const realSrc = img.dataset.src
if (!realSrc) continue
if (isVisible(img)) {
img.src = realSrc
img.dataset.src = ''
}
}
}
// 测试
window.addEventListener('load', imageLazyLoad)
window.addEventListener('scroll', imageLazyLoad)
// or
window.addEventListener('scroll', throttle(imageLazyLoad, 1000))
# 23. 手动编写一个ajax,不依赖第三方库
function ajax() {
var xhr = new XMLHttpRequest()
xhr.open('GET', '/api', false)
xhr.onreadystatechange = function(){
if (xhr.readyState === 4 && xhr.status === 200){
console.log(xhr.responceText)
}
}
xhr.send(null)
}
# 23.1. xhr.readyState 有几种状态?
- 0,未初始化(还没有调用send方法);
- 1,载入(已经调用send方法,正在发送请求);
- 2,载入完成(send方法执行完成,已经收到全部响应内容);
- 3,交互(正在解析响应的内容);
- 4,完成(响应内容解析完成,可以在客户端调用了)
# 24. 实现 a == 1 && a == 2 && a == 3
为 true
// 第一种方法
var a = {
i: 1,
toString: function () {
return a.i++;
}
}
console.log(a == 1 && a == 2 && a == 3) // true
// 第二种方法
var a = [1, 2, 3];
a.join = a.shift;
console.log(a == 1 && a == 2 && a == 3); // true
// 第三种方法
var val = 0;
Object.defineProperty(window, 'a', {
get: function () {
return ++val;
}
});
console.log(a == 1 && a == 2 && a == 3) // true
← vue-reactive 面试前端编程题 →