美文网首页前端开发那些事儿
前端常见面试题目(二)

前端常见面试题目(二)

作者: 菜菜的小阿允 | 来源:发表于2020-09-07 12:16 被阅读0次

1、redux connect()的返回值
返回一个注入了 state 和 action creator 的 React 组件。

2、事件循环机制
详情参看这篇文章,写的非常详细 https://juejin.im/post/6844904079353708557

3、key的作用
相同的节点,仅仅是位置发生了变化,但却需要进行繁杂低效的删除、创建操作,其实只要对这些节点进行位置移动即可。React针对这一现象提出了一种优化策略:允许开发者对同一层级的同组子节点,添加唯一 key 进行区分。
“key 帮助 React 识别出被修改、添加或删除的 item。应当给数组内的每个元素都设定 key,以使元素具有固定身份标识。重要的是,在前后两次渲染之间的 key 要具有“固定身份标识”的特点,以便 React 可以在添加、删除或重新排序 item 时,前后对应起来。
总结一下key的作用如下:

  • 准确判断出当前节点是否在旧集合中
  • 极大地减少遍历次数

4、react diff原理
传统diff算法通过循环递归对节点进行依次对比,效率低下,算法复杂度达到 O(n^3),其中 n 是树中节点的总数。这意味着如果要展示 1000 个节点,就要依次执行上十亿次 的比较,这种指数型的性能消耗对于前端渲染场景来说代价太高了。而React利用下面这三条diff策略,将算法时间复杂度从O(n^3)降到O(n)。
1、Web UI中DOM节点跨层级的移动操作特别少,可以忽略不计。
2、拥有相同类的两个组件将会生成相似的树形结构,拥有不同类的两个组件将会生成不同的树形结构。
3、对于同一层级的一组子节点,它们可以通过唯一 id 进行区分。
在上面三个策略的基础上,React 分别将对应的tree diff、component diff 以及 element diff 进行算法优化,极大地提升了diff效率。

  • tree diff
    React 会对树进行分层比较,两棵树只会对同一层次的节点进行比较。
    既然 DOM 节点跨层级的移动操作少到可以忽略不计,针对这一现象,React只会对相同层级的 DOM 节点进行比较,即同一个父节点下的所有子节点。当发现节点已经不存在时,则该节点及其子节点会被完全删除掉,不会用于进一步的比较。这样只需要对树进行一次遍历,便能完成整个 DOM 树的比较。
    当遇到DOM节点跨层级的操作时,React的处理如下:


    image

    A 节点(包括其子节点)整个被移动到 D 节点下,由于 React 只会简单地考虑同层级节点的位置变换,而对于不 同层级的节点,只有创建和删除操作。当根节点发现子节点中 A 消失了,就会直接销毁 A;当 D 发现多了一个子节点 A,则会创 建新的 A(包括子节点)作为其子节点。此时,diff 的执行情况:create A → create B → create C → delete A。
    由此可以发现,当出现节点跨层级移动时,并不会出现想象中的移动操作,而是以 A 为根节点的整个树被重新创建。这是一种影响React性能的操作,因此官方建议不要进行 DOM 节点跨层级的操作。
    在开发组件时,保持稳定的 DOM 结构会有助于性能的提升。例如,可以通过 CSS 隐藏或显示节点,而不是真正地移除或添加 DOM 节点。

  • component diff
    React 是基于组件构建应用的,对于组件间的比较所采取的策略如下:
    1、如果是同一类型的组件,按照原策略继续比较 Virtual DOM 树即可。
    2、 如果不是,则将该组件判断为 dirty component,从而替换整个组件下的所有子节点。
    3、 对于同一类型的组件,有可能其 Virtual DOM 没有任何变化,如果能够确切知道这点,那么就可以节省大量的 diff 运算时间。因此,React允许用户通过shouldComponentUpdate()来判断该组件是否需进行diff算法分析,但是如果调用了forceUpdate方法,shouldComponentUpdate则失效。
    转换流程如下:


    image

    当组件D变为组件G时,即使这两个组件结构相似,一旦React判断D和G是不同类型的组件,就不会比较二 者的结构,而是直接删除组件D,重新创建组件G及其子节点。虽然当两个组件是不同类型但结构相似时,diff会影响性能,但正如React官方博客所言:不同类型的组件很少存在相似DOM树的情况,因此这种极端因素很难在实际开发过程中造成重大的影响。

  • element diff
    当节点处于同一层级时,diff 提供了 3 种节点操作,分别为 INSERT_MARKUP (插入)、MOVE_EXISTING (移动)和 REMOVE_NODE (删除)。
    1、INSERT_MARKUP :新的组件类型不在旧集合里,即全新的节点,需要对新节点执行插入操作。
    2、MOVE_EXISTING :旧集合中有新组件类型,且 element 是可更新的类型,generateComponentChildren 已调用receiveComponent ,这种情况下 prevChild=nextChild ,就需要做移动操作,可以复用以前的 DOM 节点。
    3、REMOVE_NODE :旧组件类型,在新集合里也有,但对应的 element 不同则不能直接复用和更新,需要执行删除操作,或者旧组件不在新集合里的,也需要执行删除操作。
  • 新旧集合中的节点都是相同的节点的情况下
    通过key可以准确地发现新旧集合中的节点都是相同的节点,因此无需进行节点删除和创建,只需要将旧集合中节点的位置进行移动,更新为新集合中节点的位置
  • 如果新集合中有新加入的节点且旧集合存在 需要删除的节点的情况下
    需要对旧集合进行循环遍历,找出新集合中没有的节点,此时发现存在这样的节点,因此删除节点,到此 diff 操作全部完成。
    在开发过程中,尽量减少类似将最后一个节点移动到列表首部的操作。当节点数量过大或更新操作过于频繁时,这在一定程度上会影响React的渲染性能。

5、reducer纯函数的概念及作用
详情参看这篇文章,写的非常详细 https://juejin.im/post/6844903842237120519

6、继承
一、原型链继承
这种方式关键在于:子类型的原型为父类型的一个实例对象。

//父类型
function Person(name, age) {
    this.name = name,
    this.age = age,
    this.play = [1, 2, 3]
    this.setName = function () { }
}
Person.prototype.setAge = function () { }
//子类型
function Student(price) {
    this.price = price
    this.setScore = function () { }
}
Student.prototype = new Person() // 子类型的原型为父类型的一个实例对象
var s1 = new Student(15000)
var s2 = new Student(14000)
console.log(s1,s2)
//Student {price: 15000, setScore: ƒ} Student {price: 14000, setScore: ƒ}

这种方式实现的本质是通过将子类的原型指向了父类的实例,所以子类的实例就可以通过_proto_访问到 Student.prototype 也就是Person的实例,这样就可以访问到父类的私有方法,然后再通过_proto_指向父类的prototype就可以获得到父类原型上的方法。于是做到了将父类的私有、公有方法和属性都当做子类的公有属性。

子类继承父类的属性和方法是将父类的私有属性和公有方法都作为自己的公有属性和方法,我们都知道在操作基本数据类型的时候操作的是值,在操作引用数据类型的时候操作的是地址,如果说父类的私有属性中有引用类型的属性,那它被子类继承的时候会作为公有属性,这样子类1操作这个属性的时候,就会影响到子类2。

s1.play.push(4)
console.log(s1.play, s2.play)
console.log(s1.__proto__ === s2.__proto__)//true
console.log(s1.__proto__.__proto__ === s2.__proto__.__proto__)//true

s1中play属性发生变化,与此同时,s2中play属性也会跟着变化。
我们需要在子类中添加新的方法或者是重写父类的方法时候,切记一定要放到替换原型的语句之后。

function Person(name, age) {
    this.name = name,
    this.age = age
}
Person.prototype.setAge = function () {
    console.log("111")
}
function Student(price) {
    this.price = price
    this.setScore = function () { }
}
// Student.prototype.sayHello = function () { }在这里写子类的原型方法和属性是无效的,因为会改变原型的指向,所以应该放到重新指定之后
Student.prototype = new Person()
Student.prototype.sayHello = function () { }
var s1 = new Student(15000)
console.log(s1)

特点:

  • 父类新增原型方法/原型属性,子类都能访问到
  • 简单,易于实现

缺点:

  • 无法实现多继承
  • 来自原型对象的所有属性被所有实例共享
  • 创建子类实例时,无法向父类构造函数传参
  • 要想为子类新增属性和方法,必须要在Student.prototype = new Person() 之后执行,不能放到构造器中

二、构造函数继承
这种方式关键在于:在子类型构造函数中通用call()调用父类型构造函数

  function Person(name, age) {
    this.name = name,
    this.age = age,
    this.setName = function () {}
  }
  Person.prototype.setAge = function () {}
  function Student(name, age, price) {
    Person.call(this, name, age)  // 相当于: this.Person(name, age) this.name = name  this.age = age*/
    this.price = price
  }
  var s1 = new Student('Tom', 20, 15000)
  console.log(s1.setAge())//Uncaught TypeError: s1.setAge is not a function

这种方式只是实现部分的继承,如果父类的原型还有方法和属性,子类是拿不到这些方法和属性的。
特点:

  • 解决了原型链继承中子类实例共享父类引用属性的问题
  • 创建子类实例时,可以向父类传递参数
  • 可以实现多继承(call多个父类对象)

缺点:

  • 实例并不是父类的实例,只是子类的实例
  • 只能继承父类的实例属性和方法,不能继承原型属性和方法
  • 无法实现函数复用,每个子类都有父类实例函数的副本,影响性能

三、原型链+借用构造函数的组合继承
这种方式关键在于:通过调用父类构造,继承父类的属性并保留传参的优点,然后通过将父类实例作为子类原型,实现函数复用。

function Person(name, age) {
    this.name = name,
    this.age = age,
    this.setAge = function () { }
}
Person.prototype.setAge = function () {
    console.log("111")
}
function Student(name, age, price) {
    Person.call(this,name,age)
    this.price = price
    this.setScore = function () { }
}
Student.prototype = new Person()
Student.prototype.constructor = Student//组合继承也是需要修复构造函数指向的
Student.prototype.sayHello = function () { }
var s1 = new Student('Tom', 20, 15000)
var s2 = new Student('Jack', 22, 14000)
console.log(s1)
console.log(s1.constructor) //Student

这种方式融合原型链继承和构造函数的优点,是 JavaScript 中最常用的继承模式。不过也存在缺点就是无论在什么情况下,都会调用两次构造函数:一次是在创建子类型原型的时候,另一次是在子类型构造函数的内部,子类型最终会包含父类型对象的全部实例属性,但我们不得不在调用子类构造函数时重写这些属性。
优点:

  • 可以继承实例属性/方法,也可以继承原型属性/方法
  • 不存在引用属性共享问题
  • 可传参
  • 函数可复用

缺点:

  • 调用了两次父类构造函数,生成了两份实例

四、拷贝继承
这种方式关键在于:把父类公有和私有属性作为子类公有,在子类中遍历父类的实例,然后分别赋值给子类prototype

    function Parent(){
        this.name = {name:'kobe'};
    }
    Parent.prototype.pro = function(){
        console.log('prototype');
    }
    function Child(name){
        var p = new Parent();
        for(let key in p){//for in 可以把p的__proto__上的属性也可以遍历到
            Child.prototype[key] = p[key]
        }
    }

特点:

  • 可以实现多继承

缺点:

  • 效率低,占内存高
  • 无法继承父类不可枚举的方法(for in)

五、ES6中class 的继承
ES6中引入了class关键字,class可以通过extends关键字实现继承,还可以通过static关键字定义类的静态方法,这比 ES5 的通过修改原型链实现继承,要清晰和方便很多。ES5 的继承,实质是先创造子类的实例对象this,然后再将父类的方法添加到this上面(Parent.apply(this))。ES6 的继承机制完全不同,实质是先将父类实例对象的属性和方法,加到this上面(所以必须先调用super方法),然后再用子类的构造函数修改this。需要注意的是,class关键字只是原型的语法糖,JavaScript继承仍然是基于原型实现的。

class Person {
    //调用类的构造方法
    constructor(name, age) {
        this.name = name
        this.age = age
    }
    //定义一般的方法
    showName() {
        console.log("调用父类的方法")
        console.log(this.name, this.age);
    }
}
let p1 = new  Person('kobe', 39)
console.log(p1)
//定义一个子类
class Student extends Person {
    constructor(name, age, salary) {
        super(name, age)//通过super调用父类的构造方法
        this.salary = salary
    }
    showName() {//在子类自身定义方法
        console.log("调用子类的方法")
        console.log(this.name, this.age, this.salary);
    }
}
let s1 = new Student('wade', 38, 1000000000)
console.log(s1)
s1.showName()

优点:

  • 语法简单易懂,操作更方便

7、防抖和节流

  • 防抖:在事件被触发n秒后再执行回调,如果在这n秒内又被触发,则重新计时
    使用场景:对于输入框连续输入进行AJAX验证时,用函数防抖能有效减少请求次数
    简单的防抖(debounce)代码:
function debounce(fn, wait) {
  var timeout = null;
  if (timeout !== null) {
    clearTimeout(timeout)
  }
 timeout = setTimeout(fn, wait)
}
function handle () {
  console.log(Math.random())
}
window.addEventListener('scroll', debounce(handle, 2000))
  • 节流:就是指连续触发事件但是在 n 秒中只执行一次函数,节流会稀释函数的执行频率
    使用场景:鼠标点击多次,动作是有规律的在间隔时间触发一次
    简单的节流代码:
function trottle(fn, delay){
  var prev = Date.now();
  return function () {
    var context = this; 
    var args = arguments;
    var now = Date.now();
    if (now - prev >= delay) {
      fn.apply(context, args)
      prev = Date.now();
    }
  }
}
function handle(){
  console.log(Math.random());
}
window.addEventListener('scroll', throttle(handle, 2000))
  • 区别:
    函数节流不管事件触发有多频繁,都会保证在规定时间内一定会执行一次真正的事件处理函数,而函数防抖只是在最后一次事件后才触发一次函数。比如在页面的无限加载场景下,我们需要用户在滚动页面时,每隔一段时间发一次 Ajax 请求,而不是在用户停下滚动页面操作时才去请求数据。这样的场景,就适合用节流技术来实现。

相关文章

网友评论

    本文标题:前端常见面试题目(二)

    本文链接:https://www.haomeiwen.com/subject/wqlzsktx.html