面试驱动技术,每周一更(20220209期)

91 阅读7分钟

总结一下常见面试题目,跳槽也好,不跳也罢,忙也好,不忙也没关系,每周一期,自我驱动,巩固知识,卷死他们!

问题1:JS 异步解决⽅案的发展历程以及优缺点。

答案:回调函数(callback)优点:解决了同步的问题(只要有⼀个任务耗时很⻓,后⾯的任务都必须排队 等着,会拖延整个程序的执⾏。) 缺点:回调地狱,不能⽤ try catch 捕获错误,不能 return

Promise 优点:解决了回调地狱的问题 缺点:⽆法取消 Promise ,错误需要通过回调函数来捕获

Generator 特点:可以控制函数的执⾏,可以配合 co 函数库使⽤

Async/await 优点:代码清晰,不⽤像 Promise 写⼀⼤堆 then 链,处理了回调地狱的问题 缺点:await 将异步代码改造成同步代码,如果多个异步操作没有依赖性⽽使⽤ await 会导致性能上的降低。

问题2:有以下 3 个判断数组的⽅法,请分别介绍它们之间的区别和优劣

Object.prototype.toString.call() 、 instanceof 以及 Array.isArray()

答案: Object.prototype.toString.call() 常⽤于判断浏览器内置对象时。 每⼀个继承 Object 的对象都有 toString ⽅法,如果 toString ⽅法没有重写的话,会返回[object type],其中 type 为对象的类型。但当除了 Object 类型的对象外,其他类型直接使⽤ toString ⽅法时,会直接返回都是内容的字符串,所以我们需要使⽤ call 或者 apply ⽅法来改变 toString ⽅法的执⾏上下⽂。

const an = ['Hello', 'An'];

an.toString(); // "Hello,An"

Object.prototype.toString.call(an); // "[object Array]"// 这种⽅法对于所有基本的数据类型都能进⾏判断,即使是 null 和 undefined 。

Object.prototype.toString.call('An') // "[object String]"

Object.prototype.toString.call(1) // "[object Number]"

Object.prototype.toString.call(Symbol(1)) // "[object Symbol]"

Object.prototype.toString.call(null) // "[object Null]"

Object.prototype.toString.call(undefined) // "[object Undefined]"

Object.prototype.toString.call(function(){}) // "[object Function]"

Object.prototype.toString.call({name: 'An'}) // "[object Object]"

instanceof 的内部机制是通过判断对象的原型链中是不是能找到类型的prototype。

使⽤ instanceof 判断⼀个对象是否为数组,instanceof 会判断这个对象的原型链上是否会找到对应的 Array 的原型,找到返回 true,否则返回 false。

[] instanceof Array; // true// 但 instanceof 只能⽤来判断对象类型,原始类型不可以。并且所有对象类型instanceof Object 都是 true。

[] instanceof Object; // true

Array.isArray()

功能:⽤来判断对象是否为数组

instanceof 与 isArray 当检测 Array 实例时,Array.isArray 优于 instanceof ,因为 Array.isArray 可以检测出 iframes

var iframe = document.createElement('iframe');

document.body.appendChild(iframe);

xArray = window.frames[window.frames.length - 1].Array;

var arr = new xArray(1, 2, 3);

// [1,2,3]

// Correctly checking for Array

Array.isArray(arr);

// true

Object.prototype.toString.call(arr);

// true

// Considered harmful, because doesn't work though iframes

arr instanceof Array;

// false

Array.isArray() 与 Object.prototype.toString.call()

Array.isArray()是 ES5 新增的⽅法,当不存在 Array.isArray() ,可以⽤Object.prototype.toString.call() 实现。

if (!Array.isArray) {

Array.isArray = function (arg) {

return Object.prototype.toString.call(arg) === '[object Array]';

};}

问题3:ES6数组去重的⽅法

答案:

• from && set

利⽤Set集合的唯⼀性,再利⽤Array.from将类数组变回数组

let arr = [1,1,2,3]

const uniarr = Array.from(new Set(arr))

console.log(uniarr)

可以简写为

[...new Set(arr)]

• filter && indexOf

过滤数组,indexOf判断元素在数组⾸次出现的下标与当前下标是否⼀致,⼀致为唯⼀值,不⼀致为重

复元素


let arr = [1,1,2,3]

const uniarr = arr.filter((item, index) => {

return arr.indexOf(item) === index

})

console.log(uniarr)

• forEach && includes

遍历数组,如果元素不在已选列表,则将元素放进已选数组

let arr = [1,1,2,3]

let uniarr = []

arr.forEach((item) => {

if(!uniarr.includes(item)) uniarr.push(item)

})

console.log(uniarr)

• filter && Map

过滤数组,创建⼀个空的键值对集合,判断没有在集合中就返回,就保存,然后返回true,后⾯再有 重复元素,就直接返回false

Map的键可以是任意类型,Ojbect的键只能是字符串


let arr = [1,1,2,3]

let myMap = new Map()

const uniarr = arr.filter(item => {

return !myMap.has(item) && myMap.set(item, 1)

})

console.log(uniarr)

问题4:如何不使⽤loop循环,创建⼀个⻓度为100的数组,并且每个元素的值等于它的下标

答案:

Array.from(Array(100).keys())

Array(100)只是⼀个稀疏数组,⽆法⽤map去遍历。

Array(100).keys()返回的是⼀个类数组的迭代器,内容是下标

Array.from再将迭代器转成真正的数组

参考:www.zhihu.com/question/41…

问题5:requestAnimationFrame为什么能优化动画性能

答案:requestAnimationFrame ⽐起 setTimeout、setInterval的优势主要有两点:

1、requestAnimationFrame 会把每⼀帧中的所有DOM操作集中起来,在⼀次重绘或回流中就完 成,并且重绘或回流的时间间隔紧紧跟随浏览器的刷新频率,⼀般来说,这个频率为每秒60帧。

2、在隐藏或不可⻅的元素中,requestAnimationFrame将不会进⾏重绘或回流,这当然就意味着更 少的的cpu,gpu和内存使⽤量。

「CPU节能」:使⽤setTimeout实现的动画,当⻚⾯被隐藏或最⼩化时,setTimeout 仍然在后台执 ⾏动画任务,由于此时⻚⾯处于不可⻅或不可⽤状态,刷新动画是没有意义的,完全是浪费CPU资 源。⽽requestAnimationFrame则完全不同,当⻚⾯处理未激活的状态下,该⻚⾯的屏幕刷新任务 也会被系统暂停,因此跟着系统步伐⾛的requestAnimationFrame也会停⽌渲染,当⻚⾯被激活 时,动画就从上次停留的地⽅继续执⾏,有效节省了CPU开销。

「函数节流」:在⾼频率事件(resize,scroll等)中,为了防⽌在⼀个刷新间隔内发⽣多次函数执⾏,

使⽤requestAnimationFrame可保证每个刷新间隔内,函数只被执⾏⼀次,这样既能保证流畅性,

也能更好的节省函数执⾏的开销。⼀个刷新间隔内函数执⾏多次时没有意义的,因为显⽰器每

16.7ms刷新⼀次,多次绘制并不会在屏幕上体现出来。

问题6:解决JavaScript数字精度丢失问题的⽅法

答案:本质上在处理这类问题的时候,基本的思路就是通过将浮点数转换成整数进⾏计算,然后再将

整数的⼩数点位调整,转回正确的浮点数结果。

例如:

// 0.1 + 0.2

(0.110 + 0.210) / 10 == 0.3 // true

问题7:document.onload和document.ready两个事件的区别

答案:

document.onload ⻚⾯加载完成(图⽚,⽂件之类的)

document.ready ⻚⾯只加载DOM层,但未加载媒体⽂件

问题8:函数防抖与节流

答案:先看个例⼦:


<input type="text" id="unDebounce">(没有防抖的input)

<script>

function ajax (content) {

console.log('ajax request' + content)

}

let inputs = document.getElementById('unDebounce')

inputs.addEventListener('keyup', function (e) {

ajax(e.target.value)

})

</script>

只要⽤⼾按下键盘,就会不断发送请求,这样⼦会浪费资源

防抖优化

<input type="text" id="unDebounce">(有防抖的input)

<script>

function ajax (content) {

console.log('ajax request' + content)

}

function debounce (fun, delay) {

return function (args) {

    let that = this

    let _args = args

    // 清除时间

    clearTimeout(fun.id)

        fun.id = setTimeout (function () {

        fun.call(that, _args)

        }, delay)
    }
}

let input_content = document.getElementById('unDebounce')

let debounceAjax = debounce(ajax, 500)

input_content.addEventListener('keyup', function (e) {

    debounceAjax(e.target.value)

})

</script>

防抖优化之后,⽤⼾频繁输⼊不会触发请求, 只有⽤⼾输⼊间隔之后才会执⾏函数, 如果停⽌输⼊,但⼜重新输⼊,⼜会重新触发函数

函数节流

<input type="text" id="entry">(有节流的input)<script>

function ajax (content) {

console.log('ajax request ' + content + new Date())

}

function happen(fun, delay) {

let last, deferTimer

return function (args) {

let that = this

let _args = arguments

let now = +new Date()

if (last && now < last + delay) {

    clearTimeout(deferTimer)

    deferTimer = setTimeout(function () {

        last = now

        fun.apply(that, _args)

        }, delay)

     }else {

        last = now

        fun.apply(that,_args)

        }
    }
}

let input_content = document.getElementById('entry')

let throttleAjax = happen(ajax, 1000)

input_content.addEventListener('keyup', function (e) {

throttleAjax(e.target.value)

})

</script>

⽆论怎么输⼊,ajax 会按照设定好的时间,在每⼀秒发送⼀次请求。

问题9:下⾯代码的输出是什么?

(() => {

let x = (y = 10);

})();

console.log(typeof x);

console.log(typeof y);

答案:

解析:letx=y=10; 是下⾯这个表达式的缩写:

y = 10;

let x = y;

我们设定 y等于 10时,我们实际上增加了⼀个属性 y给全局对象。在浏览器中, window.y等于 10. 然 后我们声明了变量 x等于 y,也是 10.但变量是使⽤ let声明的,它只作⽤于 块级作⽤域, 仅在声明它的 块中有效;就是案例中的⽴即调⽤表达式(IIFE)。使⽤ typeof操作符时, 操作值 x没有被定义:因为我 们在 x声明块的外部,⽆法调⽤它。这就意味着 x未定义。未分配或是未声明的变量类型为 "undefined". console.log(typeofx)返回 "undefined". ⽽我们创建了全局变量 y,并且设定 y等于 10.这个值在我们的代码各处都访问的到。y已经被定义了,⽽且有⼀个 "number"类型的值。 console.log(typeofy)返回 "number".

问题10:下⾯2段代码四个数分别打印是多少?

var num1 = 2;

var num2 = 20;

var num3 = ++num1 + num2;

var num4 = num1 + num2;

console.log(num1 +'-' + num2 +'-'+ num3 +'-' + num4)

var num1 = 2;

var num2 = 20;

var num3 = num1-- + num2;

var num4 = num1 + num2;

console.log(num1 +'-' + num2 +'-'+ num3 +'-' + num4)

答案:第⼀段代码分别会打印:3-20-23-23,第⼆段代码会打印:1-20-22-21; 考察的是js⼀元操作符:递增( ++ )和递减( -- ),这两个操作符可以放在变量的前⾯,也可以放在变量 的后⾯。但是前后是有区别的。

1、变量a中,a++ 与 ++a都是对a进⾏⾃加1,结果完全相同;递减操作符都是⾃减1。

2、在赋值中使⽤前置型是先⾃加(⾃减)1,再赋值;后置型是先赋值,再⾃加(⾃减)1。

3、在计算并赋值中使⽤前置型会先进⾏⾃加或⾃减,再进⾏计算,最后赋值;后置型会先进⾏计算,再赋值,最后进⾏⾃加或⾃减。