面试刷题--003

132 阅读3分钟

一、js

1. Array.splice() 删除 替换 插入

1.1 arr.splice() 的语法


let newArr = arr.splice(起始索引index, 删除的个数, 新增元素1, 新增元素2...)   
// 返回被删除元素组成的新数组

splice 方法 有3个作用 删除数组的某一项或多项 替换数组的某些项 插入新数据

1.2 splice的删除作用 (删除原数组的值,并且返回删除元素组成的新数组)

var arr1 = ["1", "2", "3", "4", "5", "6"];
		
//传入两个参数 一个是要删除的开始位置  一个是要删除的个数
var newarr1 = arr1.splice(3, 3);

// 删除了从下标为3开始的3个值
console.log(newarr1); //["4", "5", "6"]

// 这个方法改变了原数组
console.log(arr1); //["1", "2", "3"]

var arr2 = ["1", "2", "3", "4", "5", "6"];

//传入一个参数 默认从这个参数的下标开始把数组全部删除
var newarr2 = arr2.splice(1);
console.log(newarr2); // ["2", "3", "4", "5", "6"];
console.log(arr2); // ["1"]

1.3、 插入作用 从第一个参数对应的前一个下标位置开始(传入1从下标为0后开始插入) 第二个参数书写0(不删除) 后边的参数就是插入的项

var arr1 = ["0", "1", "2", "3", "4", "5", "6"];
arr1.splice(3, 0, "a", "b", "c");

console.log(arr1); //["0", "1", "2", "a", "b", "c", "3", "4", "5", "6"]

1.4、 替换 和插入作用传入一样的参数 只不过 插入的第二个参数是不让splice删除值,而替换就是让删除的个数 和 插入的个数平衡

var arr1 = ["0", "1", "2", "3", "4", "5", "6"];
arr1.splice(3, 3, "a", "b", "c");

//可以发现从下标为3的值开始的三个值(345) 被 "a" "b" "c" 替换了
console.log(arr1); //["0", "1", "2", "a", "b", "c", "6"]

2. 洗牌算法(shuffle)的js实现

2.1 可视化实现

其算法思想就是 从原始数组中随机抽取一个新的元素到新数组中

  1. 从还没处理的数组(假如还剩n个)中,产生一个[0, n]之间的随机数 random
  2. 从剩下的n个元素中把第 random 个元素取出到新数组中
  3. 删除原数组第random个元素
  4. 重复第 2 3 步直到所有元素取完
  5. 最终返回一个新的打乱的数组

按步骤一步一步来就很简单的实现

function shuffle(arr){
    var result = [],
        random;
    while(arr.length>0){
        random = Math.floor(Math.random() * arr.length);
        result.push(arr[random])
        arr.splice(random, 1)
    }
    return result;
}

2.2 ES6

Knuth-Durstenfeld shuffle 的 ES6 实现,代码更简洁

function shuffle(arr){
    let n = arr.length, random;
    while(0!=n){
        random =  (Math.random() * n--) >>> 0; // 无符号右移位运算符向下取整
        [arr[n], arr[random]] = [arr[random], arr[n]] // ES6的结构赋值实现变量互换
    }
    return arr;
}

3. 获取数据类型

3.1 type与instanceof结合

function myTypeof(data) {
    const type = typeof data
    if (data === null) {
        return 'null'
    }
    if (type !== 'object') {
        return type
    }
    if (data instanceof Array) {
        return 'array'
    }
    return 'object'
}

3.2 Object.prototype.toString.call()

Object.prototype.toString.call(new Date()) // [object Date]
Object.prototype.toString.call("1") // [object String]
Object.prototype.toString.call(1) // [object Numer]
Object.prototype.toString.call(undefined) // [object Undefined]
Object.prototype.toString.call(null) // [object Null]

综合上述知识点,我们可以封装出以下通用类型判断方法:

function myTypeof(data) {
    var toString = Object.prototype.toString;
    var dataType = data instanceof Element ? "Element" : toString.call(data).replace(/\[object\s(.+)\]/, "$1")
    return dataType
};

获取实例化对象的类名

直接使用 xx.constructor.name 即可获取到这个数据对应的类名。

4. 让 useEffect 支持 async/await

4.1 React 为什么要这么做?

useEffect 作为 Hooks 中一个很重要的 Hooks,可以让你在函数组件中执行副作用操作。

它能够完成之前 Class Component 中的生命周期的职责。它返回的函数的执行时机如下:

  • 首次渲染不会进行清理,会在下一次渲染,清除上一次的副作用。
  • 卸载阶段也会执行清除操作。

不管是哪个,我们都不希望这个返回值是异步的,这样我们无法预知代码的执行情况,很容易出现难以定位的 Bug。

所以 React 就直接限制了不能 useEffect 回调函数中不能支持 async...await...

4.2 useEffect 怎么支持 async...await...

既然 useEffect 的回调函数不能使用 async...await,那可以直接在它内部使用。

做法一:创建一个异步函数(async...await 的方式),然后执行该函数。

useEffect(() => {
  const asyncFun = async () => {
    setPass(await mockCheck());
  };
  asyncFun();
}, []);

做法二:也可以使用 IIFE,如下所示:

useEffect(() => {
  (async () => {
    setPass(await mockCheck());
  })();
}, []);

4.3 总结与思考

由于 useEffect 是在函数式组件中承担执行副作用操作的职责,它的返回值的执行操作应该是可以预期的,而不能是一个异步函数,所以不支持回调函数 async...await 的写法。

我们可以将 async...await 的逻辑封装在 useEffect 回调函数的内部,这就是 ahooks useAsyncEffect 的实现思路,而且它的范围更加广,它支持的是所有的异步函数,包括 generator function

5. arguments 类数组,遍历类数组

5.1 类数组对象

所谓的类数组对象:

拥有一个 length 属性和若干索引属性的对象

读写、获取长度、遍历基本上和数组类似,但是无法直接调用数组方法

var arrayLike = {
    0: 'name',
    1: 'age',
    2: 'sex',
    length: 3
}

5.2 调用数组方法

既然无法直接调用,我们可以用 Function.call 间接调用:

var arrayLike = {0: 'name', 1: 'age', 2: 'sex', length: 3 }

Array.prototype.join.call(arrayLike, '&'); // name&age&sex

Array.prototype.slice.call(arrayLike, 0); // ["name", "age", "sex"] 
// slice可以做到类数组转数组

Array.prototype.map.call(arrayLike, function(item){
    return item.toUpperCase();
}); 
// ["NAME", "AGE", "SEX"]

5.3 类数组转数组

在上面的Function.call 中已经提到了一种类数组转数组的方法,再补充三个: splice.call() Array.from() concat.apply()

var arrayLike = {0: 'name', 1: 'age', 2: 'sex', length: 3 }
// 1. slice
Array.prototype.slice.call(arrayLike); // ["name", "age", "sex"] 
// 2. splice
Array.prototype.splice.call(arrayLike, 0); // ["name", "age", "sex"] 
// 3. ES6 Array.from
Array.from(arrayLike); // ["name", "age", "sex"] 
// 4. apply
Array.prototype.concat.apply([], arrayLike)

5.4 Arguments对象

Arguments 对象只定义在函数体中,包括了函数的参数和其他属性。在函数体中,arguments 指代该函数的 Arguments 对象。

5.41 length属性

Arguments对象的length属性,表示实参的长度,举个例子:

function foo(b, c, d){
    console.log("实参的长度为:" + arguments.length)
}

console.log("形参的长度为:" + foo.length)

foo(1)

// 形参的长度为:3
// 实参的长度为:1

5.42 callee属性

Arguments 对象的 callee 属性,通过它可以调用函数自身。

闭包经典面试题使用 callee 的解决方法:

var data = [];

for (var i = 0; i < 3; i++) {
    (data[i] = function () {
       console.log(arguments.callee.i) 
    }).i = i;
}

data[0]();
data[1]();
data[2]();

// 0
// 1
// 2

5.43 arguments 和对应参数的绑定

function foo(name, age, sex, hobbit) {

    console.log(name, arguments[0]); // name name

    // 改变形参
    name = 'new name';

    console.log(name, arguments[0]); // new name new name

    // 改变arguments
    arguments[1] = 'new age';

    console.log(age, arguments[1]); // new age new age

    // 测试未传入的是否会绑定
    console.log(sex); // undefined

    sex = 'new sex';

    console.log(sex, arguments[2]); // new sex undefined

    arguments[3] = 'new hobbit';

    console.log(hobbit, arguments[3]); // undefined new hobbit

}

foo('name', 'age')

传入的参数,实参和 arguments 的值会共享,当没有传入时,实参与 arguments 值不会共享

除此之外,以上是在非严格模式下,如果是在严格模式下,实参和 arguments 是不会共享的。

5.44 传递参数

将参数从一个函数传递到另一个函数

// 使用 apply 将 foo 的参数传递给 bar
function foo() {
    bar.apply(this, arguments);
}
function bar(a, b, c) {
   console.log(a, b, c);
}

foo(1, 2, 3)

5.45 ES6将arguments转成数组

使用ES6的...运算符,我们可以轻松转成数组。

function func(...arguments) {
    console.log(arguments); // [1, 2, 3]
}

func(1, 2, 3);

5.46 常见应用场景

  1. 参数不定长
  2. 函数柯里化
  3. 递归调用
  4. 函数重载 ...

6. React 性能优化

  • 使用 shouldComponentUpdate 避免不需要的渲染,但是如果对 props 和 state 做深比较,代价很大,所以需要根据业务进行些取舍;在有子组件的情况下,为了避免子组件的重复渲染,可以通过父组件来判断子组件是否需要 PureRender。
  • 将 props 设置为数组或对象:每次调用 React 组件都会创建新组件,就算传入的数组或对象的值没有改变,他们的引用地址也会发生改变,比如,如果按照如下的写法,那么每次渲染时 style 都是一个新对象
// 不推荐
<button style={{ color: 'red' }} />

// 推荐
const style = { color: 'red' }
<button style={style} />

// 不推荐
<button style={this.props.style || {} } />  

// 推荐
const defaultStyle = {}
<button style={this.props.style || defaultStyle } />   
  • 将函数的绑定移动到构造函数内:可以避免每次都绑定事件。

  • 使用 immutable 不可变数据,在我们项目中使用引用类型时,为了避免对原始数据的影响,一般建议使用 shallowCopy 和 deepCopy 对数据进行处理,但是这样会造成 CPU 和 内存的浪费,所以推荐使用 immutable,优点如下

    • 降低了“可变”带来的复杂度
    • 节省内存,immutable 使用结构共享尽量复用内存,没有被引用的对象会被垃圾回收
    • 可以更好的做撤销/重做,复制/粘贴,时间旅行
    • 不会有并发问题(因为数据本身就是不可变的)
    • 拥抱函数式编程
  • 子组件设置一个唯一的 key,因为在 diff 算法中,会用 key 作为唯一标识优化渲染

7. 自定义hook

7.1 自定义Hook

通过自定义 Hook,可以将组件逻辑提取到可重用的函数中

可以理解成Hook就是用来放一些重复代码的函数。

7.2 封装成Hook

const useList = () => {
  const [state, setState] = useState(initialState);
  const deleteLi = (index) => {
    setState((state) => {
      const newState = JSON.parse(JSON.stringify(state));
      newState.splice(index, 1);
      return newState;
    });
  };
  return { state, setState, deleteLi };//返回查、改、删
};

业务逻辑都放在useList这个函数中,并将查、改、删的API给放在一个对象中return出去。这样就形成了一个自定义Hook

7.3 使用自定义Hook

一般可以将自定义Hook给单独放在一个文件中,如果要使用,就引过来

import useList from "./useList";

function App(props) {
  const { state, deleteLi } = useList();//这里接收return出来的查、删API
  return (
 	... //这里跟最开始的App组件里是一样的
  );
}

7.4 总结

所谓的自定义Hook,实际上就是把很多重复的逻辑都放在一个函数里面,通过闭包的方式给return出来,这是非常高级的方式,程序员崇尚代码简洁,如果说以后业务开发时需要大量的重复代码,我们就可以将它封装成自定义Hook。

8. async/await 进行错误处理

(async () => {
    const fetchData = () => {
        return new Promise((resolve, reject) => {
            setTimeout(() => {
                resolve('fetch data is me')
            }, 1000)
        })
    }
    
    // const [err, data] = await fetchData().then(data => [null, data] ).catch(err => [err, null])
    
    // 抽离成公共方法
    const awaitWrap = (promise) => {
        return promise
            .then(data => [null, data])
            .catch(err => [err, null])
    }

    const [err, data] = await awaitWrap(fetchData())
    console.log('err', err)
    console.log('data', data)
    // err null
    // data fetch data is me
})()

将对 await 处理的方法抽离成公共的方法,在使用 await 调用 awaitWrap 这样的方法更优雅。如果使用 typescript 实现:

function awaitWrap<T, U = any>(promise: Promise<T>): Promise<[U | null, T | null]> {
    return promise
        .then<[null, T]>((data: T) => [null, data])
        .catch<[U, null]>(err => [err, null])
}

9. 硬件加速的原理

9.1 CPU 和 GPU 的区别

CPU 即中央处理器,GPU 即图形处理器。

两者的区别在于存在于片内的缓存体系和数字逻辑运算单元的结构差异:

  • CPU虽然有多核,但总数没有超过两位数,每个核都有足够大的缓存和足够多的数字和逻辑运算单元,并辅助有很多加速分支判断甚至更复杂的逻辑判断的硬件;
  • GPU 的核数远超CPU,被称为众核(NVIDIA Fermi有512个核)。每个核拥有的缓存大小相对小,数字逻辑运算单元也少而简单(GPU初始时在浮点计算上一直弱于CPU)。

9.2 硬件加速

常用的硬件加速方法有:

  • 最常用的方式:translate3dtranslateZ
  • opacity 属性/过渡动画(需要动画执行的过程中才会创建合成层,动画没有开始或结束后元素还会回到之前的状态)
  • will-change属性(这个知识点比较冷僻),一般配合 opacity 与 translate 使用(而且经测试,除了上述可以引发硬件加速的属性外,其它属性并不会变成复合层),作用是提前告诉浏览器要变化,这样浏览器会开始做一些优化工作(这个最好用完后就释放)
  • <video><iframe><canvas><webgl>等元素
  • 其它,譬如以前的 flash 插件

9.3 使用硬件加速的注意事项

使用硬件加速并不是十全十美的事情,比如:

  • 内存。如果GPU加载了大量的纹理,那么很容易就会发生内容问题,这一点在移动端浏览器上尤为明显,所以,一定要牢记不要让页面的每个元素都使用硬件加速。
  • 使用GPU渲染会影响字体的抗锯齿效果。这是因为GPU和CPU具有不同的渲染机制。即使最终硬件加速停止了,文本还是会在动画期间显示得很模糊。

所以不要大量使用复合图层,否则由于资源消耗过度,页面可能会变的更加卡顿。

同时,在使用硬件加速时,尽可能的使用z-index,防止浏览器默认给后续的元素创建复合层渲染。

具体的原理是这样的:

webkit CSS3中,如果一个元素添加了硬件加速,并且z-index层级比较低,那么在这个元素的后面其它元素(层级比这个元素高的,或者相同的,并且releativeabsolute属性相同的),会默认变为复合层渲染,如果处理不当会极大的影响性能。

简单点理解,可以认为是一个隐式合成的概念:如果a是一个复合图层,而且b在a上面,那么b也会被隐式转为一个复合图层,这点需要特别注意。

10. vue3设置全局变量

10.1 方法一 config.globalProperties

vue2.x挂载全局是使用 Vue.prototype.$xxxx=xxx 的形式来挂载,然后通过 this.$xxx来获取挂载到全局的变量或者方法。

这在 Vue 3 中,就等同于 config.globalProperties。这些 property 将被复制到应用中作为实例化组件的一部分。

// 之前 (Vue 2.x)
Vue.prototype.$http = () => {}

// 之后 (Vue 3.x)
const app = createApp({})
app.config.globalProperties.$http = () => {}

10.2 方法二 Provide / Inject

vue3新的 provide/inject 功能可以穿透多层组件,实现数据从父组件传递到子组件。

可以将全局变量放在根组件的 provide 中,这样所有的组件都能使用到这个变量。

如果需要变量是响应式的,就需要在 provide 的时候使用 ref 或者 reactive 包装变量。