前端知识点2

1.MVVM和MVC的区别

一、MVC

MVC模式的意思是,软件可以分成三个部分

  • 视图(View):用户界面。
  • 控制器(Controller):业务逻辑
  • 模型(Model):数据保存

各部分之间的通信方式如下。

  1. View 传送指令到 Controller
  2. Controller 完成业务逻辑后,要求 Model 改变状态
  3. Model 将新的数据发送到 View,用户得到反馈

所有通信都是单向的。

二、互动模式

接受用户指令时,MVC 可以分成两种方式。一种是通过 View 接受指令,传递给 Controller。

另一种是直接通过controller接受指令。

三、实例:Backbone

实际项目往往采用更灵活的方式,以 Backbone.js 为例。

1. 用户可以向 View 发送指令(DOM 事件),再由 View 直接要求 Model 改变状态。

2. 用户也可以直接向 Controller 发送指令(改变 URL 触发 hashChange 事件),再由 Controller 发送给 View。

3. Controller 非常薄,只起到路由的作用,而 View 非常厚,业务逻辑都部署在 View。所以,Backbone 索性取消了 Controller,只保留一个 Router(路由器) 。

四、MVP

MVP 模式将 Controller 改名为 Presenter,同时改变了通信方向。

1. 各部分之间的通信,都是双向的。

2. View 与 Model 不发生联系,都通过 Presenter 传递。

3. View 非常薄,不部署任何业务逻辑,称为"被动视图"(Passive View),即没有任何主动性,而 Presenter非常厚,所有逻辑都部署在那里。

五、MVVM

MVVM 模式将 Presenter 改名为 ViewModel,基本上与 MVP 模式完全一致。

唯一的区别是,它采用双向绑定(data-binding):View的变动,自动反映在 ViewModel,反之亦然。Angular 和 Ember 都采用这种模式。


2,js定义类的方式

在前端开发中,经常需要定义JS类。那么在JavaScript中,定义类的方式有几种,分别是什么呢?本文就JS定义类的六中方式说明如下(案例说明):

1、工厂方式

?
1
2
3
4
5
6
7
8
9
10
11
function Car(){
var ocar = new Object;
ocar.color = “blue”;
ocar.doors = 4;
ocar.showColor = function (){
document.write( this .color)
};
return ocar;
}
var car1 = Car();
var car2 = Car();

调用此函数时将创建新对象,并赋予它所有的属性和方法。使用此函数可以创建2个属性完全相同的对象。

当然可以通过给它传递参数来改版这种方式。

?
1
2
3
4
5
6
7
8
9
10
11
12
13
function Car(color,door){
var ocar = new Object;
ocar.color = color;
ocar.doors = door;
ocar.showColor = function (){
document.write( this .color)
};
return ocar;
}
var car1 = Car(“red”,4);
var car2 = Car(“blue”,4);
car1.showColor() //output:”red”
car2.showColor() //output:”blue”

现在可以通过给函数传递不同的参数来得到具有不同值的对象。

在前面的例子中,每次调用函数Car(),都要创建showcolor(),意味着每个对象都有一个自己的showcolor()方法。

但是事实上,每个对象斗共享了同一个函数。 虽然可以在函数外定义方法,然后通过将函数的属性指向该方法。

?
1
2
3
4
5
6
7
8
9
10
11
function showColor(){
 
alert( this .color);
}
function Car(){
var ocar = new Object();
ocar.color = color;
ocar.doors = door;
ocar.showColor = showColor;
return ocar;
}

但是这样看起来不像是函数的方法。

2、构造函数方式

构造函数方式同工厂方式一样简单,如下所示:

?
1
2
3
4
5
6
7
8
9
function Car(color,door){
this .color = color;
this .doors = door;
this .showColor = function (){
alert( this .color)
};
}
var car1 = new Car(“red”,4);
var car2 = new Car(“blue”,4);

可以看到构造函数方式在函数内部没有创建对象,是用this关键字。因为在调用构造函数时已经创建了对象,而在函数内部只能用this来访问对象属性。

现在用new来创建对象,看起来像那么回事了!但是它同工厂方式一样。每次调用都会为对象创建自己的方法。

3、原型方式

该方式利用了对象的prototype属性。首先用空函数创建类名,然后所有的属性和方法都被赋予prototype属性。

?
1
2
3
4
5
6
7
8
9
function Car(){
}
Car.prototype.color = “red”;
Car.prototype.doors = 4;
Car.prototype.showColor = function (){
alert( this .color);
}
var car1 = new Car();
var car2 = new Car();

在这段代码中,首先定义了一个空函数,然后通过prototype属性来定义对象的属性。调用该函数时,原型的所有属性都会立即赋予要创建的对象,所有该函数的对象存放的都是指向showColor()的指针,语法上看起来都属于同一个对象。

但是这个函数没有参数,不能通过传递参数来初始化属性,必须要在对象创建后才能改变属性的默认值。

原型方式有个很严重的问题就是当属性指向的是对象时,如数组。

?
1
2
3
4
5
6
7
8
9
10
11
12
13
function Car(){
}
Car.prototype.color = “red”;
Car.prototype.doors = 4;
Car.prototype.arr = new Array(“a”,”b”);
Car.prototype.showColor = function (){
alert( this .color);
}
var car1 = new Car();
var car2 = new Car();
car1.arr.push(“cc”);
alert(car1.arr); //output:aa,bb,cc
alert(car2.arr); //output:aa,bb,cc

这里由于数组的引用值,Car的两个对象指向的都是同一个数组,所以当在car1添加值后,在car2中也可以看到。

联合是用构造函数/原型方式就可以像其他程序设计语言一样创建对象,是用构造函数定义对象的非函数属性,用原型方式定义对象的方法。

?
1
2
3
4
5
6
7
8
9
10
11
12
13
function Car(color,door){
this .color = color;
this .doors = door;
this .arr = new Array(“aa”,”bb”);
}
Car.prototype.showColor(){
alert( this .color);
}
var car1 = new Car(“red”,4);
var car2 = new Car(“blue”,4);
car1.arr.push(“cc”);
alert(car1.arr); //output:aa,bb,cc
alert(car2.arr); //output:aa,bb

5、动态原型方式

动态原型的方式同混合的构造函数/原型方式原理相似。唯一的区别就是赋予对象方法的位置。

?
1
2
3
4
5
6
7
8
9
10
11
function Car(color,door){
this .color = color;
this .doors = door;
this .arr = new Array(“aa”,”bb”);
if ( typeof Car._initialized == “undefined”){
Car.prototype.showColor = function (){
alert( this .color);
};
Car._initialized = true ;
}
}

动态原型方式是使用一个标志来判断是否已经给原型赋予了方法。这样可以保证该方法只创建一次

6、混合工厂方式

它的目的师创建假构造函数,只返回另一种对象的新实例。

?
1
2
3
4
5
6
7
8
9
function Car(){
var ocar = new Object();
ocar.color = “red”;
ocar.doors = 4;
ocar.showColor = function (){
alert( this .color)
};
return ocar;
}

与工厂方式所不同的是,这种方式使用new运算符。

以上就是全部的创建对象方法。目前使用最广泛的就是混合构造函数/原型方式,此外,动态原型方式也很流行。在功能上与构造函数/原型方式等价。

3.js实现深浅拷贝

前言

说到深浅拷贝,必须先提到的是JavaScript的数据类型,之前的一篇文章JavaScript基础心法——数据类型说的很清楚了,这里就不多说了。

需要知道的就是一点:JavaScript的数据类型分为基本数据类型和引用数据类型。

对于基本数据类型的拷贝,并没有深浅拷贝的区别,我们所说的深浅拷贝都是对于引用数据类型而言的。

浅拷贝

浅拷贝的意思就是只复制引用,而未复制真正的值。

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const originArray = [1,2,3,4,5];
const originObj = {a: 'a' ,b: 'b' ,c:[1,2,3],d:{dd: 'dd' }};
 
const cloneArray = originArray;
const cloneObj = originObj;
 
console.log(cloneArray); // [1,2,3,4,5]
console.log(originObj); // {a:'a',b:'b',c:Array[3],d:{dd:'dd'}}
 
cloneArray.push(6);
cloneObj.a = {aa: 'aa' };
 
console.log(cloneArray); // [1,2,3,4,5,6]
console.log(originArray); // [1,2,3,4,5,6]
 
console.log(cloneObj); // {a:{aa:'aa'},b:'b',c:Array[3],d:{dd:'dd'}}
console.log(originArray); // {a:{aa:'aa'},b:'b',c:Array[3],d:{dd:'dd'}}

上面的代码是最简单的利用 = 赋值操作符实现了一个浅拷贝,可以很清楚的看到,随着 cloneArray 和 cloneObj 改变,originArray  originObj 也随着发生了变化。

深拷贝

深拷贝就是对目标的完全拷贝,不像浅拷贝那样只是复制了一层引用,就连值也都复制了。

只要进行了深拷贝,它们老死不相往来,谁也不会影响谁。

目前实现深拷贝的方法不多,主要是两种:

  1. 利用 JSON 对象中的 parse 和 stringify
  2. 利用递归来实现每一层都重新创建对象并赋值

JSON.stringify/parse的方法

先看看这两个方法吧:

The JSON.stringify() method converts a JavaScript value to a JSON string.

JSON.stringify 是将一个 JavaScript 值转成一个 JSON 字符串。

The JSON.parse() method parses a JSON string, constructing the JavaScript value or object described by the string.

JSON.parse 是将一个 JSON 字符串转成一个 JavaScript 值或对象。

很好理解吧,就是 JavaScript 值和 JSON 字符串的相互转换。

它能实现深拷贝呢?我们来试试。

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
const originArray = [1,2,3,4,5];
const cloneArray = JSON.parse(JSON.stringify(originArray));
console.log(cloneArray === originArray); // false
 
const originObj = {a: 'a' ,b: 'b' ,c:[1,2,3],d:{dd: 'dd' }};
const cloneObj = JSON.parse(JSON.stringify(originObj));
console.log(cloneObj === originObj); // false
 
cloneObj.a = 'aa' ;
cloneObj.c = [1,1,1];
cloneObj.d.dd = 'doubled' ;
 
console.log(cloneObj); // {a:'aa',b:'b',c:[1,1,1],d:{dd:'doubled'}};
console.log(originObj); // {a:'a',b:'b',c:[1,2,3],d:{dd:'dd'}};

确实是深拷贝,也很方便。但是,这个方法只能适用于一些简单的情况。比如下面这样的一个对象就不适用:

?
1
2
3
4
5
6
7
8
9
const originObj = {
  name: 'axuebin' ,
  sayHello: function (){
  console.log( 'Hello World' );
  }
}
console.log(originObj); // {name: "axuebin", sayHello: ƒ}
const cloneObj = JSON.parse(JSON.stringify(originObj));
console.log(cloneObj); // {name: "axuebin"}

发现在 cloneObj 中,有属性丢失了。。。那是为什么呢?

在 MDN 上找到了原因:

If undefined, a function, or a symbol is encountered during conversion it is either omitted (when it is found in an object) or censored to null (when it is found in an array). JSON.stringify can also just return undefined when passing in "pure" values like JSON.stringify(function(){}) or JSON.stringify(undefined).

undefinedfunctionsymbol 会在转换过程中被忽略。。。

明白了吧,就是说如果对象中含有一个函数时(很常见),就不能用这个方法进行深拷贝。

递归的方法

递归的思想就很简单了,就是对每一层的数据都实现一次 创建对象->对象赋值 的操作,简单粗暴上代码:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
function deepClone(source){
  const targetObj = source.constructor === Array ? [] : {}; // 判断复制的目标是数组还是对象
  for (let keys in source){ // 遍历目标
  if (source.hasOwnProperty(keys)){
  if (source[keys] && typeof source[keys] === 'object' ){ // 如果值是对象,就递归一下
  targetObj[keys] = source[keys].constructor === Array ? [] : {};
  targetObj[keys] = deepClone(source[keys]);
  } else { // 如果不是,就直接赋值
  targetObj[keys] = source[keys];
  }
  }
  }
  return targetObj;
}

我们来试试:

?
1
2
3
4
5
6
7
8
9
10
const originObj = {a: 'a' ,b: 'b' ,c:[1,2,3],d:{dd: 'dd' }};
const cloneObj = deepClone(originObj);
console.log(cloneObj === originObj); // false
 
cloneObj.a = 'aa' ;
cloneObj.c = [1,1,1];
cloneObj.d.dd = 'doubled' ;
 
console.log(cloneObj); // {a:'aa',b:'b',c:[1,1,1],d:{dd:'doubled'}};
console.log(originObj); // {a:'a',b:'b',c:[1,2,3],d:{dd:'dd'}};

可以。那再试试带有函数的:

?
1
2
3
4
5
6
7
8
9
const originObj = {
  name: 'axuebin' ,
  sayHello: function (){
  console.log( 'Hello World' );
  }
}
console.log(originObj); // {name: "axuebin", sayHello: ƒ}
const cloneObj = deepClone(originObj);
console.log(cloneObj); // {name: "axuebin", sayHello: ƒ}

也可以。搞定。

是不是以为这样就完了?? 当然不是。

JavaScript中的拷贝方法

我们知道在 JavaScript 中,数组有两个方法 concat 和 slice 是可以实现对原数组的拷贝的,这两个方法都不会修改原数组,而是返回一个修改后的新数组。

同时,ES6 中 引入了 Object.assgn 方法和 ... 展开运算符也能实现对对象的拷贝。

那它们是浅拷贝还是深拷贝呢?

concat

The concat() method is used to merge two or more arrays. This method does not change the existing arrays, but instead returns a new array.

该方法可以连接两个或者更多的数组,但是它不会修改已存在的数组,而是返回一个新数组。

看着这意思,很像是深拷贝啊,我们来试试:

?
1
2
3
4
5
6
const originArray = [1,2,3,4,5];
const cloneArray = originArray.concat();
 
console.log(cloneArray === originArray); // false
cloneArray.push(6); // [1,2,3,4,5,6]
console.log(originArray); [1,2,3,4,5];

看上去是深拷贝的。

我们来考虑一个问题,如果这个对象是多层的,会怎样。

?
1
2
3
4
5
6
const originArray = [1,[1,2,3],{a:1}];
const cloneArray = originArray.concat();
console.log(cloneArray === originArray); // false
cloneArray[1].push(4);
cloneArray[2].a = 2;
console.log(originArray); // [1,[1,2,3,4],{a:2}]

originArray 中含有数组 [1,2,3] 和对象 {a:1},如果我们直接修改数组和对象,不会影响 originArray,但是我们修改数组 [1,2,3] 或对象 {a:1} 时,发现 originArray 也发生了变化。

结论:concat 只是对数组的第一层进行深拷贝。

slice

The slice() method returns a shallow copy of a portion of an array into a new array object selected from begin to end (end not included). The original array will not be modified.

解释中都直接写道是 a shallow copy 了 ~

但是,并不是!

?
1
2
3
4
5
6
const originArray = [1,2,3,4,5];
const cloneArray = originArray.slice();
 
console.log(cloneArray === originArray); // false
cloneArray.push(6); // [1,2,3,4,5,6]
console.log(originArray); [1,2,3,4,5];

同样地,我们试试多层的数组。

?
1
2
3
4
5
6
const originArray = [1,[1,2,3],{a:1}];
const cloneArray = originArray.slice();
console.log(cloneArray === originArray); // false
cloneArray[1].push(4);
cloneArray[2].a = 2;
console.log(originArray); // [1,[1,2,3,4],{a:2}]

果然,结果和 concat 是一样的。

结论:slice 只是对数组的第一层进行深拷贝。

Object.assign()

The Object.assign() method is used to copy the values of all enumerable own properties from one or more source objects to a target object. It will return the target object.

复制复制复制。

那到底是浅拷贝还是深拷贝呢?

自己试试吧。。

结论:Object.assign() 拷贝的是属性值。假如源对象的属性值是一个指向对象的引用,它也只拷贝那个引用值。

... 展开运算符

?
1
2
3
4
5
6
7
8
9
10
11
12
const originArray = [1,2,3,4,5,[6,7,8]];
const originObj = {a:1,b:{bb:1}};
 
const cloneArray = [...originArray];
cloneArray[0] = 0;
cloneArray[5].push(9);
console.log(originArray); // [1,2,3,4,5,[6,7,8,9]]
 
const cloneObj = {...originObj};
cloneObj.a = 2;
cloneObj.b.bb = 2;
console.log(originObj); // {a:1,b:{bb:2}}

结论:... 实现的是对象第一层的深拷贝。后面的只是拷贝的引用值。

首层浅拷贝

我们知道了,会有一种情况,就是对目标对象的第一层进行深拷贝,然后后面的是浅拷贝,可以称作“首层浅拷贝”。

我们可以自己实现一个这样的函数:

?
1
2
3
4
5
6
7
8
9
function shallowClone(source) {
  const targetObj = source.constructor === Array ? [] : {}; // 判断复制的目标是数组还是对象
  for (let keys in source) { // 遍历目标
  if (source.hasOwnProperty(keys)) {
  targetObj[keys] = source[keys];
  }
  }
  return targetObj;
}

我们来测试一下:

?
1
2
3
4
5
6
const originObj = {a: 'a' ,b: 'b' ,c:[1,2,3],d:{dd: 'dd' }};
const cloneObj = shallowClone(originObj);
console.log(cloneObj === originObj); // false
cloneObj.a= 'aa' ;
cloneObj.c=[1,1,1];
cloneObj.d.dd= 'surprise' ;

经过上面的修改,cloneObj 不用说,肯定是 {a:'aa',b:'b',c:[1,1,1],d:{dd:'surprise'}} 了,那 originObj 呢?刚刚我们验证了 cloneObj === originObj 是 false,说明这两个对象引用地址不同啊,那应该就是修改了 cloneObj 并不影响 originObj。

?
1
2
console.log(cloneObj); // {a:'aa',b:'b',c:[1,1,1],d:{dd:'surprise'}}
console.log(originObj); // {a:'a',b:'b',c:[1,2,3],d:{dd:'surprise'}}

What happend?

originObj 中关于 a、c都没被影响,但是 d 中的一个对象被修改了。。。说好的深拷贝呢?不是引用地址都不一样了吗?

原来是这样:

  1. 从 shallowClone 的代码中我们可以看出,我们只对第一层的目标进行了 深拷贝 ,而第二层开始的目标我们是直接利用 = 赋值操作符进行拷贝的。
  2. so,第二层后的目标都只是复制了一个引用,也就是浅拷贝。

总结

  1. 赋值运算符 = 实现的是浅拷贝,只拷贝对象的引用值;
  2. JavaScript 中数组和对象自带的拷贝方法都是“首层浅拷贝”;
  3. JSON.stringify 实现的是深拷贝,但是对目标对象有要求;
  4. 若想真正意义上的深拷贝,请递归。

猜你喜欢

转载自blog.csdn.net/qq_33358062/article/details/80167150