前端面试题(每日一更)

一,什么是MVVM?
二、写React和vue项目时为什么要在列表组件中写key,其作用是什么?
三,TCP三次握手和四次挥手
四、ES5和ES6 的继承除了写法以外还有什么区别?
五,Promise 构造函数是同步执行还是异步执行,那么 then 方法呢?
六,call 和 apply 的区别是什么,哪个性能更好一些?
七,MVVM, MVP和MVC对比?各自的利弊和特点?适应哪些场景?
八,箭头函数与普通函数(function)的区别是什么?构造函数(function)可以使用 new 生成实例,那么箭头函数可以吗?为什么?
九,Vue的父组件和子组件生命周期钩子执行顺序是什么?
十,介绍下观察者模式和订阅-发布模式的区别,各自适用于什么场景
十一,请指出 和 和 .fn的区别,或者说出$.fn的用途
十二,引用类型和基本类型的区别?
十三,什么是声明提升?
十四,vue如何实现双向数据绑定?
十五,简单说下前端中的模块化开发
十六,[‘1’, ‘2’, ‘3’].map(parseInt)
十七,为什么普通 for 循环的性能远远高于 forEach 的性能,请解释其中的原因
十八,Vue 中的 computed 和 watch 的区别在哪里
十九,实现一个 $attr(name, value)遍历,属性为 name,值为 value 的元素集合

一、什么是MVVM?

MVVM是Model-View-ViewModel的缩写。MVVM是一种设计思想。Model 层代表数据模型,也可以在Model中定义数据修改和操作的业务逻辑;View 代表UI 组件,它负责将数据模型转化成UI 展现出来,ViewModel 是一个同步View 和Model的对象(桥梁)
在MVVM架构下,View 和 Model 之间并没有直接的联系,而是通过ViewModel进行交互,Model 和 ViewModel 之间的交互是双向的, 因此View 数据的变化会同步到Model中,而Model 数据的变化也会立即反应到View 上。
ViewModel 通过双向数据绑定把 View 层和 Model 层连接了起来,而View 和 Model 之间的同步工作完全是自动的,无需人为干涉,因此开发者只需关注业务逻辑,不需要手动操作DOM, 不需要关注数据状态的同步问题,复杂的数据状态维护完全由 MVVM 来统一管理。

二、写React和vue项目时为什么要在列表组件中写key,其作用是什么?

vue和React都是采用diff算法来进行新旧虚拟节点的对比,从而更新节点。在vue的diff函数中(建议先了解一下diff算法过程)。
在交叉对比中,当新节点跟旧节点头尾交叉对比没有结果时,会根据新节点的key去对比旧节点数组中的Key,从而找到相应旧节点(这里对应的是一个key => index 的map映射)。如果没有找到就认为是一个新节点。而如果没有key,那么就会采用遍历查找的方式去找到对应的旧节点。一种一个map映射,另一种是遍历查找。相比而言,map映射的速度更快。
vue部分源码如下:

// vue项目 src/core/vdom/patch.js  -488行
// 以下是为了阅读性进行格式化后的代码
// oldCh 是一个旧虚拟节点数组
if (isUndef(oldKeyToIdx)) {
    oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
}
if (isDef(newStartVnode.key)) {
    // map 方式获取
    idxInOld = oldKeyToIdx[newStartVnode.key]
} else {
    // 遍历方式获取
    idxInOld = findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)
}

创建map函数

function createKeyToOldIdx (children, beginIdx, endIdx) {
    let i, key
    const map = {}
    for (i = beginIdx; i <= endIdx; ++i) {
        key = children[i].key
        if (isDef(key)) map[key] = i
    }
    return map
}

遍历寻找

// sameVnode 是对比新旧节点是否相同的函数
function findIdxInOld (node, oldCh, start, end) {
    for (let i = start; i < end; i++) {
        const c = oldCh[i]
        if (isDef(c) && sameVnode(node, c)) return i
    }
}

也就是主要是为了提升diff【同级比较】的效率。自己想一下自己要实现前后列表的diff,如果对列表的每一项增加一个key,即唯一索引,那就可以很清楚的知道两个列表谁少了,谁没变。而如果不加key的话,就只能一个个对比了。
说到底,key的作用就是更新组件时判断两个节点是否相同。相同就复用,不相同就删除旧的创建新的。

三、TCP三次握手和四次挥手

TCP三次握手:

  1. 客户端发送请求到服务器,等待服务器确认接收。
  2. 服务器确认接收到请求,并返回一个客户端一个消息
  3. 客户端确认接收到服务器的回复以后,再次向服务器发送确认消息,二者建立联系后,完成了TCP三次握手。
  4. 四次挥手就是中间多了一层,等待服务器再一次响应回复相关数据的过程。
  5. 三次握手是为了双方建立链接而做得信息确认(发送seq和ack),确认信息后即可建立连接开始通信。
  6. 四次挥手是因为需要单边发起关闭请求和关闭响应,然后断开连接。
    TCP三次握手机制中的seq和ack的值到底是什么?
    seq是序列号,这是为了连接以后传送数据用的,ack是对收到的数据包的确认,值是等待接收的数据包的序列号。
    seq是数据包本身的序列号,ack是期望对方继续发送的那个数据包的序列号。

四、ES5和ES6 的继承除了写法以外还有什么区别?

  1. class声明会提升,但不会初始化赋值。Foo进入暂时性死区,类似于let、const声明变量。
// es5
const bar = new Bar();    // it`s ok
function Bar() {
    this.bar = 42;
}

// ES6
const foo = new Foo();    // ReferenceError: Foo is not defined
class Foo {
    constructor() {
        this.foo = 42;
    }
}

  1. class声明内部会启用严格模式。
// 声明内部会启用严格模式
// ES5
function Bar () {
    baz = 42;    // it`s ok
}
const bar = new Bar();

// es6
class Foo {
    constructor () {
        fol = 42;    // ReferenceError: Foo is not defined
    }
}
const foo = new Foo();
  1. class的所有方法(包括静态方法和实例方法)都是不可枚举的。
// ES5 引用一个未声明的变量
function Bar () {
    this.bar = 42;
}
Bar.answer = function () {
    return 42;
}
Bar.prototype.print = function () {
    console.log(this.bar);
};
const barKeys = Object.keys(Bar);   // ['answer']
const barProtoKeys = Object.keys(Bar.prototype);  // ['print']
// ES6
class Foo {
    constructor () {
        this.foo = 42;
    }
    static answer () {
        return 42;
    }
    print () {
        console.log(this.foo);
    }
}
const fooKeys = Object.keys(Foo);    // []
const fooProtoKeys = Object.keys(Foo.prototype);  // []
  1. class的所有方法(包括静态方法和实例方法)都没有原型对象prototype,所以也没有[[ construct ]],不能使用new来调用。
// ES5
function Bar () {
    this.bar = 42;
}
Bar.prototype.print = function () {
    console.log(this.bar);
};
const bar = new Bar();
const barPrint = new bar.print();  // it`s ok

// ES6
class Foo {
    constructor () {
        this.foo = 42;
    }
    print () {
        console.log(this.foo);
    }
}
const foo = new Foo();
const fooPrint = new foo.print(); // TypeError: foo.print is not a constructor
  1. 必须使用new调用class
// ES5
function Bar () {
    this.bar = 42;
}
const bar = Bar(); // it`s ok
// ES6
class Foo {
    constructor () {
        this.foo = 42;
    }
}
const foo = Foo();// TypeError: Class constructor Foo cannot be invoked without 'new'
  1. class内部无法重写类名
// ES5
function Bar() {
    Bar = 'Baz';    // it`s ok
    this.bar = 42;
}
const bar = new Bar();
// Bar: 'Baz'
// bar: Bar {bar: 42}

// ES6
class Foo {
    constructor () {
        this.foo = 42;
        Foo = 'Fol'; // TypeError: Assignment to constant variable
    }
}
const foo = new Foo();
Foo = 'Fol';    // it`s ok

JavaScript相对比其他面向类的语言,在实现继承时并没有真正对构造类进行复制,当我们使用var children = new Parent()继承父类时,我们理所当然的理解为children“为parent所构造”。这是一种错误的理解。
严格来说,JS才是真正的面向对象语言,而不是面向类语言。它所实现的继承,都是通过每个对象创建之初就存在的prototype属性进行关联、委托,从而建立联系,间接的实现继承,实际上不会复制父类。
ES5最常见的两种继承:原型链继承、构造函数继承
1). 原型链继承

// 定义父类
function Parent(name) {
    this.name = name;
}

Parent.prototype.getName = function () {
    return this.name;
}

// 定义子类
function Children () {
    this.age = 24;
}

// 通过Children的prototype属性和parent进行关联继承
Children.prototype = new Parent('陈先生');
var test = new Children();
test.age   // 24
test.getName();  // 陈先生

我们可以发现,整个继承过程,都是通过原型链之间的指向进行委托关联,直到最后形成了“由构造函数所构造的”结局。
2). 构造函数继承

// 定义父类
function Parent(value) {
    this.language = ['javascript', 'react', 'node.js'];
    this.value = value;
}
// 定义子类
function Children () {
    Parent.apply(this, arguments);
}

const test = new Children(666);

test.language   // ['javascript', 'react', 'node.js']
test.value  // 666

构造函数关键在于,通过子类的内部调用父类,即通过使用apply()或者call()方法可以在将来新创建的对象上获取父类的成员和方法。

ES6的继承

// 定义父类
class Father {
    constructor (name, age) {
        this.name = name;
        this.age = age;
    }
    show () {
        console.log(`我叫${this.name},今年${this.age}`);
    }
}

// 通过extends关键字来实现继承
class Son extends Father {};
let son = new Son('陈先生', 3000);
son.show()    // 我叫陈先生,今年3000岁

总结:
区别在于ES6的继承实现在于使用super关键字调用父类,反观ES5是通过call或者apply回调方法调用父类。
实际上无论是ES5的prototype模拟类还是ES6的语法糖class,都不是真正意义上的类。因为在类的实现中,子类是对父类的完全复制,而js不是,换句话说,如果我们在改变了JS一个父类的方法,继承该父类的子类和所有实例都会发生改变。ES6class的实现,本质上还是通过Object.create()去关联两者的prototype。

五,Promise 构造函数是同步执行还是异步执行,那么 then 方法呢?

来看一段代码:

const promise = new Promise((resolve, reject) => {
    console.log(1)
    resolve()
    console.log(2)
})

promise.then(() => {
    console.log(3)
})

console.log(4)
// 执行结果   1243

promise构造函数是同步执行的,then放到是异步执行的。
再对上面的例子做一个扩展:

const promise = new Promise((resolve, reject) => {
  console.log(1);
  resolve(5);
  console.log(2);
}).then(val => {
  console.log(val);
});

promise.then(() => {
  console.log(3);
});

console.log(4);

setTimeout(function() {
  console.log(6);
});

// 执行结果  124536

then其实是同步执行的,只不过是then里的cb被放入了微任务队列,产生了异步执行。

六,call 和 apply 的区别是什么,哪个性能更好一些?

Function.prototype.apply和Function.prototype.call得作用是一样得,区别在于传入参数的不同:

  1. 第一个参数都是指向函数体内this的指向,也就是运行函数的作用域;

  2. 第二个参数开始不同,apply是传入带下标的集合,数组或者类数组,apply把它传给函数作为参数,call从第二个开始传入的参数是不固定的,都会传给函数作为参数。

  3. call比apply的性能要好,平常可以多用call,call传入参数的格式正是内部所需要的格式。

  4. 尤其是ES6引入了Spread operator(延展操作符)后,即使参数是数组,可以使用call

    let params = [1,2,3,4]
    xx.call(obj, …params)

七,MVVM, MVP和MVC对比?各自的利弊和特点?适应哪些场景?

看图(我自己公众号的图)

  1. 特点
    在这里插入图片描述
    在这里插入图片描述
  2. 对比
    首先是MVC:
    在这里插入图片描述
    然后是MVP:
    在这里插入图片描述
    最后是MVVM:
    在这里插入图片描述
    附加一点vue的一些主要特性:
    在这里插入图片描述

八,箭头函数与普通函数(function)的区别是什么?构造函数(function)可以使用 new 生成实例,那么箭头函数可以吗?为什么?

箭头函数是普通函数的简写,可以更优雅的定义一个函数,和普通函数相比,有一下几点差异:

  1. 函数体内的this对象,就是定义时所在的对象,而不是使用时所在的对象。
  2. 不可以使用arguments对象,该对象在函数体内不存在。如果要用,可以用rest参数代替。
  3. 不可以使用yield命令,因此箭头函数不能用作Generator函数。

那么箭头函数是不可以使用new命令,因为:

  1. 没有自己的this,无法调用call和apply;
  2. 没有prototype属性,而new命令在执行时需要将构造函数的prototype赋值给新的对象的_proto_;

new过程大致时这样的:

   function newFunc(father, ...rest) {
        var result = {};
        // 在new命令执行时将构造函数的prototype赋值给新对象的_proto_
        result._proto_ = father.prototype;
        // 使用apply来改变this指向
        var result2 = father.apply(result, rest);
        if (typeof result2 === 'Object' || typeof result2 === 'Function' 
        && result2 !== null) {
            return result2;
        }
        return result;
    }

那么箭头函数的语法极为简练,且在没有箭头函数的时候,函数闭包var that = this 的事情没少干,有了箭头函数,就不需要这么写了。

九,Vue的父组件和子组件生命周期钩子执行顺序是什么?

  1. 父组件: beforeCreate -> created -> beforeMount
  2. 子组件: ->beforeCreate -> created -> beforeMount -> mounted
  3. 父组件: -> mounted

总结: 从外到内,再从内到外

  • 加载渲染过程

    父beforeCreate->父created->父brforeMount->子beforeCreate->子created->
    beforeMount->子mounted->父mounted

  • 子组件更新过程

    父beforeUpdate->子beforeUpdate->子updated->父updated

  • 父组件更新过程

    父beforeUpdate->父updated

  • 销毁过程

    父beforeDestroy->子beforeDestroy->子destroyed->父destroyed

十,介绍下观察者模式和订阅-发布模式的区别,各自适用于什么场景?

观察者模式中主体和观察者是相互感知的,发布-订阅者模式是借助第三方来实现调度的,发布者和订阅者之间不感知。

在这里插入图片描述
举个例子来说:

  1. 发布-订阅者模式就好像报社,邮局和个人的关系,报纸的订阅和分发都是由邮局来完成的。报社只负责将报纸发送给邮局。
  2. 观察者模式就好像个人和个体奶农的关系,奶农负责统计有多少人订了产品,所以个人都会有一个相同拿牛奶的方法。奶农有了新奶就负责调用这个方法。

更为简单粗暴的方式来说就是:

  1. 观察者模式没有中间商赚差价;
  2. 发布订阅模式是有中间商赚差价的

观察者模式由具体目标调度,每个被订阅的目标里面都需要有对观察者的处理,会造成代码的冗余。而发布订阅模式则统一由调度中心处理,消除了发布者和订阅者之间的依赖。

十一,请指出$和$.fn的区别,或者说出$.fn的用途

jQuery为开发插件提供了两个方法,分别是: $.extend(obj); $.fn.extend(obj); 那么这两个分别是什么意思呢? $.extend(obj)是为了扩展jQuery本身,为类添加新的方法。 $.fn.extend(obj)给jQuery对象添加方法 $.fn中的fn是什么意思?其实就是prototype,即$.fn = $.prototype 具体用法看下面例子:
$.extend({
    add:function(a, b) {
        return a+b;
    }
})
$.add(5,8); // return 13

注意这里是直接调用,前面不用任何对象。直接$+方法名。(ajax的用法)
$.fn.extend(obj)是对prototype进行扩展,为jQuery类添加成员函数,jQuery类的实例可以使用这个成员函数。

$.fn.extend({
    clickwhile:function(){
        $(this).click(function(){
            alert($(this).val())
        })
    }
})
$('input').clickwhile(); // 当点击输入框会弹出该对象的Value值

注意调用时前面是有对象的。即$(‘input’)这个DOM。

公众号:Coder 杂谈,欢迎关注
在这里插入图片描述

おすすめ

転載: blog.csdn.net/qq_42345237/article/details/99690420