前端开发那些事儿

前端常见面试题目(二)

2020-09-07  本文已影响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效率。

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)

特点:

缺点:

二、构造函数继承
这种方式关键在于:在子类型构造函数中通用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

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

缺点:

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

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]
        }
    }

特点:

缺点:

五、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、防抖和节流

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))
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))
上一篇下一篇

猜你喜欢

热点阅读