JS 经典面试题
# # JS 经典面试题
重点考察JS原理及应用
# # 1. call/apply
# # 问题
var foo = { value: 1 }
function bar() { console.log(this.value) }
bar.call(foo) // 期待打印:1
bar.apply(foo) // 期待打印:1
2
3
4
5
# # 思路
call/apply立即执行函数,同时函数中的this改为指向context。类似等价于以下
var foo = {
value: 1,
fn: function bar() { console.log(this.value) }
}
2
3
4
5
Function.prototype.call = function(context, ...args) {
context = context || window
context.fn = this // 这里的this代表函数
context.fn(...args) // 给context添加属性fn,所以执行fn方法时,里面的this代表context
delete context.fn
}
Function.prototype.apply = function(context, ...args) {
context = context || window
context.fn = this
context.fn(args) // apply传递数组
delete context.fn
}
2
3
4
5
6
7
8
9
10
11
12
13
14
# # 2. bind
# # 问题
var foo = { value: 1 }
function bar() { console.log(this.value) }
let barBind = bar.bind(foo)
barBind() // 期待打印:1
2
3
4
5
# # 思路
通过apply改变this,并且返回一个函数
Function.prototype.bind = function (context, ...args) {
var fn = this
return function() {
return fn.apply(context, args)
}
}
2
3
4
5
6
7
# # 3. new
# # 问题
new Foo('name') = _new(Foo, 'name') // 模拟new
2
# # 思路
_new() {
var object = new Object() // 1. 类都是object类型
var Constructor = [].shift.call(arguments)
var args = arguments // 剩下的参数
object.__proto__ = Constructor.prototype // 2. 设置原型链
var ret = Constructor.apply(obj, args) // 3. 构造函数执行
return typeof ret === 'object' ? ret : obj
}
_new(Foo, 'name')
// es6
_new(Constructor, ...args) {
let object = Object.create(Constructor.prototype)
let ret = Constructor.apply(object, args)
return typeof ret === 'object' ? ret : obj
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# # 4. curry
# # 问题
let addFun = function(a, b, c) { return a + b + c }
let curryFun = curry(addFun)
curryFun(1)(2)(3) === 6 // true
2
3
4
# # 思路
递归,当执行的参数个数等于原本函数的个数,执行函数
var curry = function(fn) {
var limit = fn.length // fn函数参数个数
return function judgeCurry (...args) {
if (args.length >= limit) {
return fn.apply(null, args)
} else {
return function(...args2) {
return judgeCurry.apply(null, args.concat(args2))
}
}
}
}
// or es6
var curry = function(fn, ...args) {
if (args.length >= fn.length) {
return fn(...args)
}
return function (...args2) {
return curry(fn, ...args, ...args2)
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# # 变种思考
这道题也有很多变种,比如如下考察闭包:实现curry(1,3, 4)(1, 2)(1)() = 12
const curry = (...arg) => {
return (...arg2) => {
if (arg2.length === 0) {
return arg.reduce((sum, val) => sum += val, 0)
} else {
return curry(...arg, ...arg2)
}
}
}
console.log(curry(1,3, 4)(1, 2)(1)()) // 12
2
3
4
5
6
7
8
9
10
11
12
# # 5. pipe/compose
# # pipe
- pipe(fn1,fn2,fn3,fn4)(args)等价于fn4(fn3(fn2(fn1(args)))
- 第一个函数的结果,作为第二个函数的参数,以此类推...
# # compose
- compose(fn1,fn2,fn3,fn4)(args)等价于fn1(fn2(fn3(fn4(args)))
- 与pipe相反,先计算倒数第一个结果,作为倒数第二的参数,以此类推...
let loopItem = (prevFn, nextFn) => (...args) => prevFn(nextFn(...args))
const compose = (...fns) => fns.reduce(loopItem);
const pipe = (...fns) => fns.reduceRight(loopItem)
const example = pipe(
(x, y) => x * y,
x => x + 1
);
console.log(example(3, 4)) // 13
2
3
4
5
6
7
8
9
10
11
# # 6. flatten
# # 深度为1的展平
// before:[1, 2, [3, 4, [5, 6]]]
// after flat: [1, 2, 3, 4, [5, 6]]
// 思路:使用reduce或map
function flatSingle(arr) {
return arr.reduce((pre, val) => pre.concat(val), [])
}
// or
let flatSingle = arr => [].concat(...arr)
2
3
4
5
6
7
8
9
10
11
# # 深度无限的展平
// before: [1,2,3,[1,2,3,4, [2,3,4]]]
// after flatDeep: [1, 2, 3, 1, 2, 3, 4, 2, 3, 4]
// 思路:深度优先递归,使用reduce连接起来
// 深度优先算法 - 递归
function flatDeep(arr) {
return arr.reduce((pre, val) => pre.concat(Array.isArray(val) ? flatDeep(val) : val), [])
}
// 深度优先算法 - 堆栈
function flatDeep(arr) {
const stack = [...arr]
const res = []
while (stack.length) {
const val = stack.pop() // 从尾部开始
Array.isArray(val) ? stack.push(...val) : res.push(val)
}
return res.reverse()
}
// 取巧,利用Array.toString()
function flatDeep(arr) {
return arr.toString().split(',')
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# # 指定深度的展平
深度的含义是指每一项展平的次数
// before: [1,2,3,[1, [2]], [1, [2, [3]]]]
// after: [ 1, 2, 3, 1, 2, 1, 2, [ 3 ] ]
function flatDeep(arr, depth = 1) {
if (depth === 1) return arr.reduce((pre, val) => pre.concat(val), [])
return arr.reduce((pre, val) => pre.concat(Array.isArray(val) ? flatDeep(val, depth - 1) : val), [])
}
2
3
4
5
6
7
8
# # 7. 去重
数组去除重复
// before: [2, 1, 3, 2]
// after: [2, 1, 3]
function removeRepeat(arr) {
return arr.filter((item, index) => arr.indexOf(item) === index)
}
// or es6
let removeRepeat = arr => Array.from(new Set(arr))
let removeRepeat = arr => [...new Set(arr)]
2
3
4
5
6
7
8
9
10
11
# # 8. 浅拷贝/深拷贝
// 浅拷贝
function clone(source) {
var target = {}
for (var i in source) {
source.hasOwnProperty(i) && target[i] = source[i]
}
return target
}
// or es6
const clone = source => Object.assign({}, source)
const clone = source => { ...source }
2
3
4
5
6
7
8
9
10
11
12
13
14
// 深拷贝
// 思路:递归赋值
const deepClone = obj => {
const isObj = obj => typeof obj === 'object' || typeof obj === 'function' && obj !== null
if (!isObj(obj)) {
throw new Error('不是对象')
}
// 区分array和object对象
let target = Array.isArray(obj) ? [] : {}
// https://stackoverflow.com/questions/34449045/what-is-the-difference-between-reflect-ownkeysobj-and-object-keysobj
Reflect.ownKeys(obj).forEach(key => {
target[key] = isObj(obj[key]) ? deepClone(obj[key]) : obj[key]
})
return target
}
// 优化:以上未考虑到对象循环引用
const isObject = obj => obj !== null && (typeof obj === 'object' || typeof obj === 'function');
const isFunction = obj => typeof obj === 'function'
function deepClone (obj, hash = new WeakMap()) {
if (hash.get(obj)) {
// 环处理
return hash.get(obj);
}
if (!isObject(obj)) {
// 基本数据处理
return obj;
}
if (isFunction(obj)) {
// function返回原引用
return obj;
}
let cloneObj;
const Constructor = obj.constructor;
switch (Constructor) {
// 包装函数处理,可能是new Boolean(false)
case Boolean:
case Date:
return new Date(+obj);
case Number:
case String:
case RegExp:
return new Constructor(obj);
default:
cloneObj = new Constructor(); // 重要:初始化cloneObj类型
hash.set(obj, cloneObj);
}
[...Object.getOwnPropertyNames(obj), ...Object.getOwnPropertySymbols(obj)].forEach(k => {
cloneObj[k] = deepClone(obj[k], hash);
})
return cloneObj;
}
// or 取巧方法
// 注意这种取巧方法是有限制的
// 1. 只能解析Number、String、Array等能够被json表示的数据结构
// 2. 不能处理循环引用
const deepClone = source => JSON.parse(JSON.stringify(source))
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
# # 9. 防抖/节流
- 防抖:在事件被触发n秒后再执行回调,如果在这n秒内又被触发,则重新计时。适合多次事件一次响应。
- 节流:规定一个单位时间,在这个单位时间内,只能有一次触发事件的回调函数执行,如果在同一个单位时间内某事件被触发多次,只有一次能生效。适合大量事件按时间做平均分配触发。
// 防抖案例:窗口resize停止才执行最终的函数
function debounce(fn, wait, ...args) {
var that = this
fn.tId && clearTimeout(fn.tId)
fn.tId = setTimeout(function() {
fn.apply(that, args)
}, wait)
}
function handle(e) {
console.log('resize end event')
console.log(e) // Event{}
}
// 缺点:handle不能写成匿名函数,因为把tId存储在handle函数对象上。所以间接导致传递参数e较为复杂
window.onresize = function(e) { debounce(handle, 1000, e) }
// 改进版
// 思路: 用闭包把tId存储起来
function debounce(fn, wait) {
var tId
return function() {
var that = this
var args = arguments
tId && clearTimeout(tId)
tId = setTimeout(function() {
fn.apply(that, args)
}, wait)
}
}
function handle(e) {
console.log('resize end event')
console.log(e) // Event{}
}
window.onresize = debounce(handle, 1000)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// 节流案例: 不停scroll时,滚动条每隔100ms固定频率执行函数
function throttle(fn, wait) {
var cur = new Date()
fn.last = fn.last || 0
if (cur - fn.last > wait) {
fn.call()
fn.last = cur
}
}
function handle() {
console.log('scroll event')
}
window.onscroll = function() { throttle(handle, 100) }
// 或者
function throttle(fn, wait) {
var canRun = true // 标记是否可继续执行
return function() {
var that = this
var args = arguments
if (!canRun) return
canRun = false
setTimeout(function() {
fn.apply(that, args)
canRun = true
}, wait)
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# # 10. 判断变量是 Proxy 的实例
核心知识:Symbol.toStringTag (opens new window) (opens new window)
toString() 方法只能返回特定类型的标签。一些对象类型(如Map,Promise)能toString()识别是因为引擎为它们设置好toStringTag标签
Object.prototype.toString.call(new Map()); // "[object Map]"
Object.prototype.toString.call(function* () {}); // "[object GeneratorFunction]"
Object.prototype.toString.call(Promise.resolve()); // "[object Promise]"
// 自定义toStringTag
({[Symbol.toStringTag]: 'Foo'}.toString()) // [object Foo]
2
3
4
5
6
7
# # 动手实现判断Proxy思路:
- Symbol.toStringTag 属性来改写 Object.prototype.toString 方法
- Proxy代理construct()方法,设置 Symbol.toStringTag
// 通过 Proxy 对 Proxy 本身做代理,然后赋值给 Proxy
Proxy = new Proxy(Proxy, {
construct: function(target, args) {
let result = new target(...args) // target为Proxy
const originTag = Object.prototype.toString.call(result).slice(1,-1).split(' ')[1]
result[Symbol.toStringTag] = 'Proxy-' + originTag
return result
}
})
// 测试
let testProxy = new Proxy([], {})
Object.prototype.toString.call(testProxy) // [object Proxy-Array]
2
3
4
5
6
7
8
9
10
11
12
13
14
# # 11. 手动实现模版引擎
# # 问题
手动实现模版引擎,类似underscore
// 实现underscore模板引擎
<script type="text/html" id="user_tmpl">
<%for ( var i = 0; i < users.length; i++ ) { %>
<li>
<a href="<%=users[i].url%>">
<%=users[i].name%>
</a>
</li>
<% } %>
</script>
var precompile = _.template(document.getElementById("user_tmpl").innerHTML);
var html = precompile(data);
2
3
4
5
6
7
8
9
10
11
12
13
14
# # 思路
- 利用正则替换字符。
- 将 %> 替换成 p.push('
- 将 <% 替换成 ');
- 将 <%=xxx%> 替换成 ');p.push(xxx);p.push('
- 利用eval/Function执行字符代码。
// 按以上规则形成如下:
');for ( var i = 0; i < users.length; i++ ) { p.push('
<li>
<a href="');p.push(users[i].url);p.push('">
');p.push(users[i].name);p.push('
</a>
</li>
'); } p.push('
2
3
4
5
6
7
8
9
function tmpl(str, data) {
var string = "var p = []; p.push('" +
str
.replace(/[\r\t\n]/g, "")
.replace(/<%=(.*?)%>/g, "');p.push($1);p.push('")
.replace(/<%/g, "');")
.replace(/%>/g,"p.push('")
+ "');"
eval(string)
return p.join('');
};
2
3
4
5
6
7
8
9
10
11
12
13
14
# # 12. Injector
# # 问题
模拟RequireJS的模块。
injector.register('service', function(name) { return name })
injector.register('constant', 2)
var func = injector.resolve(['service', 'constant'], function(service,constant, other) {
console.log(service(constant), other)
});
func(3) // 期待打印:2, 3
2
3
4
5
6
7
# # 思路
通过闭包把register存储的值当作参数注入到func中。
笔者注:这种方式可以自己注册模块并且选择是否在流程中使用它,典型的依赖注入
(也叫控制反转(IOC),因为本来func是用户依赖创建的,主动权在func上,但现在里面的逻辑是靠传入的service/serviceN参数确定的,相当于把主动权让渡给参数了,所以叫控制反转)。
// 依赖注入(控制反转)
var injector = {
dependencies: {},
register: function(key, value) {
this.dependencies[key] = value;
},
resolve: function(deps, func, scope) {
var registerArgs = deps.map(name => this.dependencies[name])
return function(...args) {
func.apply(scope || {}, [...registerArgs, ...args])
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
# # 13. 实现ajax.get()
说明:考查Promise能力
ajax.get = function(url) {
return new Promise(function(resolve, reject) {
let xhr = new XMLHttpRequest()
xhr.open('get', url, true)
xhr.onreadystatechange = function() {
if (this.readyState === 4) {
if (this.status === 200) {
resolve(this.response, this)
} else {
reject({ response: this.response, code: this.status})
}
}
}
xhr.send()
})
}
get('http://api.wwnight.cn').then((value, that)=>{
console.log(value)
})
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
更进一步,实现 Promise 的catch、finally、race、all实现,Promise更多细节内容可看笔者另外一篇文章:动手实现Promise (opens new window) (opens new window):
# # 1. Promise.prototype.catch
Promise.prototype.catch = function(onRejected) {
return this.then(null, onRejected)
}
2
3
4
# # 2. Promise.prototype.finally(cb)
# # finally分析
- finally(cb)方法中的回调函数,都会执行
- finally后依然可以添加then方法,then参数值为上一个then的返回值
let p1 = new Promise((resolve, reject) => {
resolve('value')
})
p1.then(val => 'next-' + val)
.finally(() => console.log('finally'))
.then(val => console.log(val)) // 'next-value'
2
3
4
5
6
7
8
# # 思路
finally() 等同于 .then(onFinally, onFinally)
Promise.prototype.finally = function (cb) {
const Promise = this.constructor
return this.then(
val => Promise.resolve(cb()).then(() => value),
err => Promise.resolve(cb()).then(() => throw err)
)
}
2
3
4
5
6
7
8
# # 3. Promise.all
Promise.all可以将多个Promise实例包装成一个新的Promise实例。同时,成功和失败的返回值是不同的,成功的时候返回的是一个结果数组,而失败的时候则返回最先被reject失败状态的值(失败优先)。
Promise.all = (promises) => {
return new Promise((onResolve, onReject) => {
let result = [], ;
// promise为空时,直接return
if (!promises.length) {
onResolve(result)
return
}
let pending = i = promises.length
let processPromise = (i) => {
promises[i].then(value => {
result[i] = value // 收集结果
// 当收集完时,onResolve返回
if (!--pending) {
onResolve(result)
}
}, onReject)
}
while(i--) {
processPromise(i)
}
})
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# # 4. Promise.race
race就是赛跑的意思。哪个结果获得的快,就返回那个结果,不管结果本身是成功状态还是失败状态。
Promise.race = (promises) => {
return new Promise((onResolve, onReject) => {
for (p of promises) {
// 谁快谁先onResolve输出
Promise.resolve(p).then(onResolve, onReject)
}
})
}
2
3
4
5
6
7
8
9
# # 14. 图片懒加载
// 方法一
function inSight(el) {
const bound = el.getBoundingClientRect()
const height = window.innerHeight
return bound.top < height
}
const imgs = document.querySelectorAll('.my-photo')
function checkImg() {
console.log(1)
imgs.forEach(img => {
if (inSight(img)) {
loadImg(img)
}
})
}
function loadImg(el) {
if (!el.src) {
const source = el.dataset.src
el.src = source
}
}
function throttle(fn, wait = 100) {
let pre
return function () {
if (!pre) {
pre = +new Date
}
let now = +new Date
if (now - pre > wait) {
pre = now
fn()
}
}
}
window.onscroll = throttle(checkImg)
// 方法二
function checkImgs(){
Array.from(document.querySelectorAll('.my-photo')).forEach(item => io.observe(item))
}
function loadImg(el){
if(!el.src){
const source = el.dataset.src
el.src = source
}
}
const io = new IntersectionObserver(ioes => {
ioes.forEach((ioe)=>{
const el = ioe.target
const intersectionRatio = ioe.intersectionRatio
if(intersectionRatio > 0 && intersectionRatio <= 1){
loadImg(el)
}
el.onload = el.onerror = ()=>io.unobserve(el)
})
})
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
# # 15. 手写发布订阅/依赖者模式
前端必备设计模式案例
// 发布订阅
const event = {
obj: {},
on: function(name, fn) {
(this.obj[name] || this.obj[name] = []).push(fn)
},
emit: function(name, ...args) {
if (Array.isArray(this.obj[name])) {
this.obj[name].forEach(fn => fn.call(this, ...args))
}
}
}
// 依赖者模式
function Dep() {
this.watchers = []
}
Dep.prototype.depend = watcher => this.watchers.push(watcher)
Dep.prototype.notify = () => this.watchers.forEach(w => w.update())
function Watcher(fn) {
this.fn = fn
}
Watcher.prototype.update = function() {
this.fn()
}
const dep = new Dep()
dep.depend(new Watcher(function() {}))
dep.notify()
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# # 参考文章
- 30-seconds-of-code (opens new window) (opens new window)
- MDN Function.prototype.bind() (opens new window) (opens new window)
- 一行写出javascript函数式编程中的curry (opens new window) (opens new window)
- ES6 JavaScript Compose Function (opens new window) (opens new window)
- JS函数防抖和函数节流 (opens new window) (opens new window)
- Dependency injection in JavaScript (opens new window) (opens new window)