一,什么是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三次握手:
- 客户端发送请求到服务器,等待服务器确认接收。
- 服务器确认接收到请求,并返回一个客户端一个消息
- 客户端确认接收到服务器的回复以后,再次向服务器发送确认消息,二者建立联系后,完成了TCP三次握手。
- 四次挥手就是中间多了一层,等待服务器再一次响应回复相关数据的过程。
- 三次握手是为了双方建立链接而做得信息确认(发送seq和ack),确认信息后即可建立连接开始通信。
- 四次挥手是因为需要单边发起关闭请求和关闭响应,然后断开连接。
TCP三次握手机制中的seq和ack的值到底是什么?
seq是序列号,这是为了连接以后传送数据用的,ack是对收到的数据包的确认,值是等待接收的数据包的序列号。
seq是数据包本身的序列号,ack是期望对方继续发送的那个数据包的序列号。
四、ES5和ES6 的继承除了写法以外还有什么区别?
- 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;
}
}
- 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();
- 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); // []
- 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
- 必须使用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'
- 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得作用是一样得,区别在于传入参数的不同:
-
第一个参数都是指向函数体内this的指向,也就是运行函数的作用域;
-
第二个参数开始不同,apply是传入带下标的集合,数组或者类数组,apply把它传给函数作为参数,call从第二个开始传入的参数是不固定的,都会传给函数作为参数。
-
call比apply的性能要好,平常可以多用call,call传入参数的格式正是内部所需要的格式。
-
尤其是ES6引入了Spread operator(延展操作符)后,即使参数是数组,可以使用call
let params = [1,2,3,4]
xx.call(obj, …params)
七,MVVM, MVP和MVC对比?各自的利弊和特点?适应哪些场景?
看图(我自己公众号的图)
- 特点
- 对比
首先是MVC:
然后是MVP:
最后是MVVM:
附加一点vue的一些主要特性:
八,箭头函数与普通函数(function)的区别是什么?构造函数(function)可以使用 new 生成实例,那么箭头函数可以吗?为什么?
箭头函数是普通函数的简写,可以更优雅的定义一个函数,和普通函数相比,有一下几点差异:
- 函数体内的this对象,就是定义时所在的对象,而不是使用时所在的对象。
- 不可以使用arguments对象,该对象在函数体内不存在。如果要用,可以用rest参数代替。
- 不可以使用yield命令,因此箭头函数不能用作Generator函数。
那么箭头函数是不可以使用new命令,因为:
- 没有自己的this,无法调用call和apply;
- 没有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的父组件和子组件生命周期钩子执行顺序是什么?
- 父组件: beforeCreate -> created -> beforeMount
- 子组件: ->beforeCreate -> created -> beforeMount -> mounted
- 父组件: -> mounted
总结: 从外到内,再从内到外
-
加载渲染过程
父beforeCreate->父created->父brforeMount->子beforeCreate->子created->
beforeMount->子mounted->父mounted -
子组件更新过程
父beforeUpdate->子beforeUpdate->子updated->父updated
-
父组件更新过程
父beforeUpdate->父updated
-
销毁过程
父beforeDestroy->子beforeDestroy->子destroyed->父destroyed
十,介绍下观察者模式和订阅-发布模式的区别,各自适用于什么场景?
观察者模式中主体和观察者是相互感知的,发布-订阅者模式是借助第三方来实现调度的,发布者和订阅者之间不感知。
举个例子来说:
- 发布-订阅者模式就好像报社,邮局和个人的关系,报纸的订阅和分发都是由邮局来完成的。报社只负责将报纸发送给邮局。
- 观察者模式就好像个人和个体奶农的关系,奶农负责统计有多少人订了产品,所以个人都会有一个相同拿牛奶的方法。奶农有了新奶就负责调用这个方法。
更为简单粗暴的方式来说就是:
- 观察者模式没有中间商赚差价;
- 发布订阅模式是有中间商赚差价的
观察者模式由具体目标调度,每个被订阅的目标里面都需要有对观察者的处理,会造成代码的冗余。而发布订阅模式则统一由调度中心处理,消除了发布者和订阅者之间的依赖。
十一,请指出$和$.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 杂谈,欢迎关注