linzx


  • 首页

  • 归档

  • 标签

  • 搜索

js中数组的原生方法

发表于 2017-09-03 | 分类于 javascript | 阅读次数
| 字数统计 7,515 | 阅读时长 30

数组应该是我们在写程序中应用到最多的数据结构了,相比于无序的对象,有序的数组帮我们在处理数据时,实在是帮了太多的忙了。今天刚好看到一篇Array.include的文章,忽然发现经过几个ES3,ES5,ES6,ES7几个版本的更迭,发现在代码中用到了好多数组的方法,所以准备全部列出来,也是给自己加深印象

1 ES3中的数组方法

  • ES3兼容现在所有主流浏览器

ES3中的方法毫无疑问大家已经烂熟在心了,不过中间有些细节可以回顾加深一下记忆,比如是否修改原数组返回新数组,执行方法之后的返回值是什么,某些参数的意义是否搞混等等。熟悉的的可以直接快速浏览或者跳过。

1.1 join()方法

Array.join()方法是将一个数组里面的所有元素转换成字符串,然后再将他们连接起来返回一个新数组。可以传入一个可选的字符串来分隔结果字符串中的所有元素。如果没有指定分隔字符串,就默认使用逗号分隔。

1
2
3
let a = [1,2,3,4,5,6,7];
let b = a.join(); // b = "1,2,3,4,5,6,7";
let c = a.a.join(" "); // b = "1 2 3 4 5 6 7";

方法Array.join()恰好与String.split()相反,后者是通过将一个字符串分隔成几个元素来创建数组

1.2 reverse()方法

Array.reverse()方法将颠倒数组中元素的顺序并返回一个颠倒后的数组。它在原数组上执行这一操作,所以说并不是创建了一个新数组,而是在已存在的数组中对元素进行重排。

1
2
let a = [1,2,3,4,5,6,7];
a.reverse(); // a = [7,6,5,4,3,2,1]

1.3 sort()方法

Array.sort()是在原数组上进行排序,返回排序后的数组。如果调用方法时不传入参数,那么它将按照字母顺序对数组元素进行排序,说得更精确点,是按照字符编码的顺序进行排序。要实现这一点,首先应把数组的元素都转换成字符串(如有必要),以便进行比较。

如果数组中有未定义的元素,这些元素将放在数组的末尾

1
2
3
let a = [1,12,23,14,,undefined,null,NaN,56,6,7,"a",{},[]];
a.sort(); //[[], 1, 12, 14, 23, 56, 6, 7, "NaN", {}, "a", null,undefined,undefined × 1]
//返回的NaN已经是一个字符串,说明在比较过程中将其转化成了字符串进行比较

仔细看可以发现,上面顺序并没有按照数字大小进行排序。如果想按照其他标准进行排序,就需要提供比较函数。该函数比较前后两个值,然后返回一个用于说明这两个值的相对顺序的数字。比较函数应该具有两个参数 a 和 b,其返回值如下:

  • 若 a 小于 b,在排序后的数组中 a 应该出现在 b 之前,则返回一个小于 0 的值。
  • 若 a 等于 b,则返回 0。
  • 若 a 大于 b,在排序后的数组中 a 应该出现在 b 之后,则返回一个大于 0 的值。
    1
    2
    let a = [1,12,23,14,,undefined,null,NaN,56,6,7,"a",{},[]];
    a.sort((a,b) => {return a - b}); //[null, Array(0), NaN, Object, 1, 6, 7, 12, 14, 23, 56, "a",undefined, undefined × 1]

1.4 concat()方法

Array.concat() 方法用于连接两个或多个参数(数组,字符串等),该方法不会改变现有的数组,而会返回连接多个参数的一个新数组。如果传入的参数是数组,那么它将被展开,将元素添加到返回的数组中。但要注意,concat并不能递归的展开一个元素为数组的参数。

1
2
let a = [1,2,3];
let b = a.concat(4,5,[6,7,[9,10]]); // b = [1,2,3,4,5,6,7,[9,10]]];

1.5 slice()方法

Array.slice() 方法可从已有的数组中返回指定的一个片段(slice),或者说是子数组。它是从原数组中截取了一个片段,并返回到了一个新数组。

Array.slice(a,b) 它有两个参数a,b

参数 描述
a 必选。规定从何处开始选取。如果是负数,那么它规定从数组尾部开始算起的位置。也就是说,-1 指最后一个元素,-2 指倒数第二个元素,以此类推。
b 可选。规定从何处结束选取。该参数是数组片断结束处的数组下标。如果没有指定该参数,那么切分的数组包含从 start 到数组结束的所有元素。如果这个参数是负数,那么它规定的是从数组尾部开始算起的元素。
1
2
3
4
5
let a = [1,2,3,4,5,7,8];
let b = a.slice(3); // [4, 5, 7, 8]
let c = a.slice(3,5); // [4, 5]
let d = a.slice(-5,-2); // [3, 4, 5]
let d = a.slice(2,1); // []

请注意,该方法并不会修改数组,而是返回一个新的子数组。如果想删除数组中的一段元素,应该使用下面这个方法 Array.splice()。

1.6 splice()方法

Array.splice() 方法从数组中添加/删除元素,然后返回被删除的元素。它在原数组上修改数组,并不像slice和concat那样创建新数组。注意,虽然splice和slice名字非常相似,但是执行的却是完全不同的操作。

参数 描述
index 必选,整数。规定添加/删除项目的位置,使用负数可从数组结尾处倒着寻找位置。
howmany 可选,整数。要删除的元素数量。如果设置为 0,则不会删除元素。如果没有选择,则默认从index开始到数组结束的所有元素
item1, …, itemX 可选。向数组添加新的元素。
1
2
3
4
5
6
7
8
let a = [1,2,3,4,5,7,8];
let b = a.splice(3); // a = [1,2,3] b = [4, 5, 7, 8]
-----------------------------------------------------------
let c = [1,2,3,4,5,7,8];
let d = c.splice(3,5); // c = [1,2] d = [3,4,5,7,8]
-----------------------------------------------------------
let e = [1,2,3,4,5,7,8];
let f = e.splice(3,2,111,222,[1,2]); // e = [1, 2, 3, 111, 222,[1,2], 7, 8] f = [4,5]

大家要记住slice()和splice()两个方法第二个参数代表的意义是不一样的。虽然这很基础,可是有时候还是会弄混。

1.7 push()和pop()方法

Array.push() 方法可向数组的末尾添加一个或多个元素,并返回新的长度。

Array.pop()方法用于删除并返回数组的最后一个元素。如果数组已经为空,则 pop() 不改变数组,并返回 undefined 值。

1
2
3
let a = [1,2,3,4,5];
let b = a.pop(); //a = [1,2,3,4] b = 5
let c = a.push(1,3,5); // a = [1,2,3,4,1,3,5] c = 7

上面两个方法都是直接对原数组进行操作。通过上面两个方法可以实现一个先进后出的栈。

1.8 unshift和shift()方法

unshift,shift()的方法行为和push(),pop()非常相似,只不过他们是对数组的头部元素进行插入和删除。

Array.unshift() 方法可向数组的头部添加一个或多个元素,并返回新的长度。

Array.shift()方法用于删除并返回数组的第一个元素。如果数组已经为空,则 pop() 不改变数组,并返回 undefined 值。

1
2
3
let a = [1,2,3,4,5];
let b = a.shift(); //a = [2,3,4,5] b = 1
let c = a.unshift(1,3,5); // a = [1,3,5,2,3,45] c = 7

1.9 toString()和toLocaleString()方法

和所有javascript的对象一样,数组也有toString()方法,这个方法可以将数组的每一个元素转化成字符串(如果必要的话,就调用元素的toString()方法),然后输出字符串的列表,字符串之间用逗号隔开。(用我的话来理解,其实就是遍历数组元素调用每个元素自身的toString()方法,然后用逗号连接)

toString()的返回值和没有参数的join()方法返回的字符串相同

1
2
let a = let e = [1,undefined,null,Boolean,{},[],function(){console.log(1);}];
let b = a.toString(); // b = "1,,,function Boolean() { [native code] },[object Object],,function (){console.log(1);}"

注意,输出的结果中,返回的数组值周围没有括号。

toLocaleString方法是toString()方法的本地化版本。它是使用地区特定的分隔符把生成的字符串连接起来,形成一个字符串。

虽然是两个方法,但是一般元素两个方法的输出结果却基本是一样的,去网上找了相关文章,发现只有两种情况比较有区分,一个是时间,一个是4位数字以上的数字,举例如下

1
2
3
4
5
6
7
let a = 1111;
let b = a.toLocaleString(); // b = "1,111"
let c = a.toString(); // c = "1111";
-------------------------------------------------------
let date = new Date();
let d = date.toString(); // d = "Sun Sep 03 2017 21:52:18 GMT+0800 (中国标准时间)"
let e = date.toLocaleString(); //e = "2017/9/3 下午9:52:18"

好吧,这个api和数组关系不大。。。主要还是和数组中元素自身有关。啊哈哈,尴尬。

1.10 valueOf()

Array.valueOf()方法在日常中用的比较少,该方法继承与Object。javascript中许多内置对象都针对自身重写了该方法,数组Array.valueOf()直接返回自身。

1
2
3
let a = [1,"1",{},[]];
let b = a.valueOf();
a === b; // true


好啦,关于ES3的方法就不详细描述了,我相信大家基本上都已经完全是烂熟于心的那种,唯一可能需要加强记忆的就是一些参数含义,返回数据这些了。


2 ES5中的数组方法

  1. ES5中的数组方法在各大浏览器的兼容性
  • Opera 11+
  • Firefox 3.6+
  • Safari 5+
  • Chrome 8+
  • Internet Explorer 9+

2.Array在ES5新增的方法中接受两个参数,第一个参数都是function类型,必选,默认有传参,这些参数分别是:

  • currentValue : 数组当前项的值
  • index : 数组当前项的索引
  • array : 数组对象本身

第二个参数是当执行回调函数时指向的this(参考对象),不提供默认为window,严格模式下为undefined。

以forEach举例

语法

1
2
3
4
5
array.forEach(callback, thisArg)
array.forEach(callback(currentValue, index, array){
//do something
}, thisArg)

例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//demo,注意this指向
//我这个demo没有用箭头函数来测试
let a = ['a', 'b', 'c'];
a.forEach(function(currentValue, index, array){
this.info(currentValue, index, array);
},{info:function(value,index,array){
console.log(`当前值${value},下标${index},数组${array}`)}
});
function info(value,index,array){
console.log(`外放方法 : 当前值${value},下标${index},数组${array}`)}
}
// 当前值a,下标0,数组a,b,c
// 当前值b,下标1,数组a,b,c
// 当前值c,下标2,数组a,b,c

3.ES5中的所有关于遍历的方法按升序为数组中含有效值的每一项执行一次callback函数,那些已删除(使用delete方法等情况)或者未初始化的项将被跳过(但不包括那些值为 undefined 的项)(例如在稀疏数组上)。

例子:数组哪些项被跳过了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function logArrayElements(element, index, array) {
console.log(`a[${index}] = ${element}`);
}
let xxx; //定义未赋值
let a = [1,2,"", ,undefined,xxx,3];
delete a[1]; // 移除 2
a.forEach(logArrayElements);
// a[0] = 1
// 注意索引1被跳过了,因为在数组的这个位置没有项 被删除了
// a[2] = ""
// 注意索引3被跳过了,因为在数组的这个位置没有项,可以理解成没有被初始化
// a[4] = undefined
// a[5] = undefined
// a[6] = 3

好了,上面3点基本上是ES5中所有方法的共性,下面就不重复述说了。开始正文解析每个方法的不同了


2.1 forEach()

Array.forEach() 为每个数组元素执行callback函数;不像map() 或者reduce() ,它总是返回 undefined值,并且不可链式调用。典型用例是在一个链的最后执行副作用。

注意: 没有办法中止或者跳出 forEach 循环,除了抛出一个异常。如果你需要跳出函数,推荐使用Array.some。如果可以,新方法 find() 或者findIndex() 也可被用于真值测试的提早终止。

如果数组在迭代时被修改了

下面的例子输出”one”, “two”, “three”。当到达包含值”two”的项时,整个数组添加了一个项在第一位,这导致所有的元素下移一个位置。此时在下次执行回调中,因为元素 “two”符合条件,结果一直增加元素,直到遍历次数完毕。forEach()不会在迭代之前创建数组的副本。

1
2
3
4
5
6
7
8
9
10
11
let a = ["one", "two", "three"];
let b = a.forEach((value,index,arr) => {
if (value === "two") {
a.unshift("zero");
}
return "new" + value
});
// one,0,["one", "two", "three"]
// two,1,["one", "two", "three"]
// two,2,["zero", "one", "two", "three"]
// two,3,["zero","zero", "one", "two", "three"]

看完例子可以发现,使用 forEach 方法处理数组时,数组元素的范围是在callback方法第一次调用之前就已经确定了。在 forEach 方法执行的过程中:原数组中新增加的元素将不会被 callback 访问到;若已经存在的元素被改变或删除了,则它们的传递到 callback 的值是 forEach 方法遍历到它们的那一个索引时的值。

2.2 map()

Array.map 方法会给原数组中的每个元素都按顺序调用一次callback函数。callback每次执行后的返回值(没有指定返回值则返回undefined)组合起来形成一个新数组。

例子:返回每个元素的平方根的数组

1
2
3
4
5
let a = [1,4,9];
let b = a.map((value) => {
return Math.sqrt(value); //如果没有return,则默认返回undefined
});
// b= [1,2,3]

2.3 filter()

Array.filter()为数组中的每个元素调用一次 callback 函数,并利用所有使得 callback 返回 true 或 等价于 true 的值 的元素创建一个新数组。那些没有通过 callback 测试的元素会被跳过,不会被包含在新数组中

例子:数组去重

1
2
3
4
5
let a = [1,2,3,4,32,6,79,0,1,1,8];
let b = a.filter((value,index,arr) => {
return arr.indexOf(value) === index;
});
// b = [1, 2, 3, 4, 32, 6, 79, 0, 8]

2.4 some()

Array.some 为数组中的每一个元素执行一次 callback 函数,直到找到一个使得 callback 返回一个“真值”(即可转换为布尔值 true 的值)。如果找到了这样一个值,some 将会立即返回 true。否则,some 返回 false。callback 只会在那些”有值“的索引上被调用,不会在那些被删除或从来未被赋值的索引上调用。

例子:查看数组内是否含有大于0的元素

1
2
3
4
5
let a = [-1,4,9];
let b = a.some((value) => {
return value > 0; //如果没有return,则默认返回undefined,将无法告诉some判断
});
// b = true

some方法可以理解成拥有跳出功能的forEach()函数,可以用在在一些需要中断函数的地方

2.5 every()

Array.every() 方法为数组中的每个元素执行一次 callback 函数,直到它找到一个使 callback 返回 false(表示可转换为布尔值 false 的值)的元素。如果发现了一个这样的元素,every 方法将会立即返回 false。否则,callback 为每一个元素返回 true,every 就会返回 true。callback 只会为那些已经被赋值的索引调用。不会为那些被删除或从来没被赋值的索引调用。

例子:检测所有数组元素的大小,是否都大于0

1
2
3
4
5
let a = [-1,4,9];
let b = a.every((value) => {
return value > 0; //如果没有return,则默认返回undefined
});
// b = false

2.6 indexOf()

Array.indexOf()使用严格相等(strict equality,即===)进行判断searchElement与数组中包含的元素之间的关系。

Array.indexOf()提供了两个参数,第一个searchElement代表要查询的元素,第二个代表fromIndex表示从哪个下标开始查找,默认为0。

语法

1
2
arr.indexOf(searchElement)
arr.indexOf(searchElement, fromIndex = 0)

Array.indexOf()会返回首个被找到的元素在数组中的索引位置; 若没有找到则返回 -1

例子:

1
2
3
4
5
6
let array = [2, 5, 9];
array.indexOf(2); // 0
array.indexOf(7); // -1
array.indexOf(9, 2); // 2
array.indexOf(2, -1); // -1
array.indexOf(2, -3); // 0

2.7 lastIndexOf()

Array.lastIndexOf()就不细说了,其实从名字大家也可以看出来,indexOf是正向顺序查找,lastIndexOf是反向从尾部开始查找,但是返回的索引下标仍然是正向的顺序索引
。
语法

1
arr.lastIndexOf(searchElement, fromIndex = arr.length - 1)

需要注意的是,只是查找的方向相反,fromIndex和返回的索引都是正向顺序的,千万不要搞混了(感觉我这么一说,大家可能搞混了,捂脸)。

例子:各种情况下的的indexOf

1
2
3
4
5
6
7
var array = [2, 5, 9, 2];
var index = array.lastIndexOf(2); // index = 3
index = array.lastIndexOf(7); // index = -1
index = array.lastIndexOf(2, 3); // index = 3
index = array.lastIndexOf(2, 2); // index = 0
index = array.lastIndexOf(2, -2); // index = 0
index = array.lastIndexOf(2, -1); // index = 3

2.8 reduce()

Array.reduce() 为数组中的每一个元素依次执行回调函数,最后返回一个函数累计处理的结果。

语法

1
array.reduce(function(accumulator, currentValue, currentIndex, array), initialValue)

reduce的回调函数中的参数与前面的不同,多了第一个参数,是上一次的返回值

  • accumulator : 上一次调用回调返回的值,或者是提供的初始值(initialValue)
  • currentValue : 数组当前项的值
  • currentIndex : 数据当前项的索引。第一次遍历时,如果提供了 initialValue ,从0开始;否则从1开始
  • array : 调用 reduce 的数组
  • initialValue : 可选项,其值用于第一次调用 callback 的第一个参数。如果没有设置初始值,则将数组中的第一个元素作为初始值。空数组调用reduce时没有设置初始值将会报错。

例子:数组求和

1
2
3
4
let sum = [0, 1, 2, 3].reduce(function (o,n) {
return o + n;
});
// sum = 6

对了,当回调函数第一次执行时,accumulator 和 currentValue 的取值有两种情况:

  • 调用 reduce 时提供initialValue,accumulator 取值为 initialValue ,currentValue 取数组中的第一个值;
  • 没有提供 initialValue ,accumulator 取数组中的第一个值,currentValue 取数组中的第二个值。

例子:reduce数组去重

1
2
3
4
5
6
7
8
[1,2,3,4,5,6,78,4,3,2,21,1].reduce(function(accumulator,currentValue){
if(accumulator.indexOf(currentValue) > -1){
return accumulator;
}else{
accumulator.push(currentValue);
return accumulator;
}
},[])

注意 :如果数组为空并且没有提供initialValue, 会抛出TypeError 。如果数组仅有一个元素并且没有提供initialValue, 或者有提供initialValue但是数组为空,那么此唯一值将被返回并且callback不会被执行。

2.9 reduceRight()方法

Array.reduceRight() 为数组中的每一个元素依次执行回调函数,方向相反,从右到左,最后返回一个函数累计处理的结果。

因为这个方法和reduce方法基本是一模一样的,除了方法相反,所以就不详细的再写一遍了

2.10 isArray()方法

之所以将这个方法放在最后,是因为这个方法和前面的不太一致,是用于确定传递的值是否是一个 Array,使用方法也很简单

例子

1
2
let a = Array.isArray([1,2,3]); //true
let b = Array.isArray(document.getElementsByTagName("body")); //类数组也为false

不过感觉除非是临时判断,不然一般也不会用这个方法去判断,一般还是下面这种万金油型的吧。

1
Object.prototype.toString.call([]).slice(8, -1) === "Array";//true

好啦,关于ES5的方法基本上就讲到这里了,感觉自己在深入去看了一些文章之后,还是有一些额外的收获的。比如对reduce这个平时不常用的方法了解更加深刻了,感觉之前很多遍历收集数据的场景其实用reduce更加方便。


3 ES6中的数组方法

不同于es5主要以遍历方法为主,es6的方法是各式各样的,不过必须要说一句,在性能上,es6的效率基本上是最低的。

3.1 …方法——concat方法的增强

英文名字叫做Spread syntax,中文名字叫做扩展运算符。

3.2 of()方法

Array.of()方法可以将传入参数以顺序的方式返回成一个新数组的元素。

1
let a = Array.of(1, 2, 3); // a = [1, 2, 3]

其实,刚看到这个api和他的用途,还是比较懵逼的,因为看上去这个方法就是直接将传入的参数变成一个数组之外,就没有任何区别了,那么我为什么不直接用以前的写法去实现类似的效果呢,比如 let = [1,2,3];而且看上去也更加直接。然后我去翻了下最新的ECMAScript草案,其中有这么一句话

The of function is an intentionally generic factory method; it does not require that its this value be the Array constructor. Therefore it can be transferred to or inherited by other constructors that may be called with a single numeric argument.

自己理解了一下,其实大概意思就是说为了弥补Array构造函数传入单个函数的不足,所以出了一个of这个更加通用的方法,举个例子

1
2
let a = new Array(1);//a = [undefined × 1]
let b = new Array(1,2);// b = [1,2]

大家可以注意到传入一个参数和传入两个参数的结果,完全是不一样的,这就很尴尬了。而为了避免这种尴尬,es6则出了一种通用的of方法,不管你传入了几个参数,都是一种相同类型的输出结果。

不过我好奇的是,如果只传入几个参数,为什么不直接let a = [1,2,3];效率和直观性也更加的高。如果要创建一个长度的数组,我肯定还是选let a = new Array(10000),这种形式,实在没有感觉到Array.of的实用场景,希望大家可以给我点指导。

3.2 from()方法

Array.from()方法从一个类似数组(拥有一个 length 属性和若干索引属性的任意对象)或可迭代的对象(String, Array, Map, Set和 Generator)中创建一个新的数组实例。

我们先查看Array.from()的语法

语法

1
Array.from(arrayLike, mapFn, thisArg)

从语法中,我们可以看出Array.from()最基本的功能是将一个类数组的对象转化成数组,然后通过第二个和第三个参数可以对转化成功后的数组再次执行一次遍历数据map方法,也就是Array.from(obj).map(mapFn, thisArg)。

对了额外说一句,这个方法的性能很差,和直接的for循环的性能对比了一下,差了百倍不止。

例子 :将一串数字字符串转化为数组

1
2
let a = Array.from("242365463432",(value) => return value * 2);
//a = [4, 8, 4, 6, 12, 10, 8, 12, 6, 8, 6, 4]

3.4 copyWithin()方法

Array.copyWithin方法,在当前数组内部,将指定位置的成员浅复制到其他位置(会覆盖原有成员),然后返回当前数组。也就是说,使用这个方法,会修改当前数组。

这个方法有点复杂,光看描述可能大家未必能轻易理解,大家可以先看下语法,再看demo配合理解,而且自己没有想到这个方法合适的应用场景。网上也没又看到相关使用场景。但是讲道理,这个方法设计出来,肯定是经过深思熟虑的,如果大家有想到,欢迎评论给我,谢谢。

语法

1
2
arr.copyWithin(target, start, end)
//arr.copyWithin(目标索引, 源开始索引, 结束源索引)

例子

1
2
3
4
5
6
7
8
// 将3号位复制到0号位
[1, 2, 3, 4, 5].copyWithin(0, 3, 4); // [4, 2, 3, 4, 5]
// 将2号位复制到0号位
[1, 2, 3, 4, 5].copyWithin(0, 2, 5); //[3, 4, 5, 4, 5]
// 将3号位复制到0号位
[1, 2, 3, 4, 5].copyWithin(4, 1, 4); //[1, 2, 3, 4, 2]

第一个是常规的例子,大家可以对比看第二个可以发现,这个方法是先浅复制了数组一部分暂时存储起来,然后再从目标索引处开始一个个覆盖后面的元素,直到这段复制的数组片段全部粘贴完。

再看第三个例子,可以发现当复制的数据片段从目标索引开始粘贴时,如果超过了长度,它将停止粘贴,这说明它不会改变数据的 length,但是会改变数据本身的内容。

Array.copyWithin可以理解成复制以及粘贴序列这两者是为一体的操作;即使复制和粘贴区域重叠,粘贴的序列也会有拷贝来的值。

3.5 find() 和 findIndex()方法

Array.find()方法返回数组中满足提供的测试函数的第一个元素的值。否则返回 undefined。
Array.findIndex() 方法返回数组中满足提供的测试函数的第一个元素的值的索引。否则返回 -1。

这两个方法其实使用非常相似,使用场景有点像ES5中Array.some,都是在找到第一个满足条件的时候,跳出循环,区别的是,三种返回的值完全不一样,我想这也许是为什么要在ES6中增加这两个API的原因吧,可以理解成是数组的方法的补足。

例子:三个方法各自的返回值

1
2
3
4
5
6
7
8
let a = [1,2,3,4,5].find((item)=>{return item > 3}); // a = 4 返回第一个符合结果的值
let b = [1,2,3,4,5].findIndex((item)=>{return item > 3}); // b = 3 返回第一个符合结果的下标
let c = [1,2,3,4,5].some((item)=>{return item > 3}); // c = true 返回是否有符合条件的Boolean值
-----------------不满足条件--------------------
let a = [1,2,3,4,5].find((item)=>{return item > 6}); // a = undefined
let b = [1,2,3,4,5].findIndex((item)=>{return item > 6}); // b = -1
let c = [1,2,3,4,5].some((item)=>{return item > 6}); // c = false

注意:find()和findIndex()方法无法判断NaN,可以说是内部用 ===判断,不同于ES7中的include方法。不过这个判断方式是另外一个话题,不在本文详述了,感兴趣的同学可以去查一下。

其实还可以发现,Array.find() 方法只是返回第一个符合条件的元素,它的增强版是es5中Array.filter()方法,返回所有符合条件的元素到一个新数组中。可以说是当用find方法时考虑跟多的是跳出吧。

我感觉这4个方法配合相应的回调函数基本上可以完全覆盖大多数需要数组判断的场景了,大家觉得呢?

3.5 fill方法

Array.fill()方法用一个固定值填充一个数组中从起始索引到终止索引内的全部元素,返回原数组

这个方法的使用也非常简单,大家基本上看个语法和demo就能懂了。需要注意的是,这个方法是返回数组本身,还有一点就是,类数组不能调用这个方法,刚刚自己去改了MDN上面的文档。

语法

1
2
3
arr.fill(value)
arr.fill(value, startIndex)
arr.fill(value, startIndex, endIndex)

例子

1
2
3
4
let a = new Array(10);
a.fill(1); // a = [1, 1, 1, 1, 1, 1, 1, 1, 1, 1];
let b = [1,2,34,5,6,7,8].fill(3,4); //b = [1, 2, 34, 5, 3, 3, 3];
let c = [1,2,34,5,6,7,8].fill(3,2,5); // c = [1, 2, 3, 3, 3, 7, 8];

个人感觉这个方法初始化数组挺有用的,自己一周每次测试数据时,只要new Array().fill(1);,比以前遍历直观方便多了

3.6 entries(),keys(),values()方法

Array.entries()将数组转化成一个中包含每个索引的键/值对的Array Iterator对象

Array.keys()将数组转化成一个中包含每个索引的键的Array Iterator对象

Array.values()将数组转化成一个中包含每个索引的值的Array Iterator对象。

Array.values()方法chrome浏览器并不支持,

之所以将这三个方法放在一起是有原因的额,大家可以看这三个方法其实都是一个数组转化为一种新的数据类型——返回新的Array Iterator对象,唯一区别的是转化之后的元素不一样。跟他们的名字一样,entries()方法转化为全部的键值对,key()方法转化为键,value()保留值。

例子:观察各个迭代器遍历输出的东西

Array.entries()

1
2
3
4
5
let a = [1,2,3].entries();
for(let i of a){console.log(i);}
//[0, 1]
//[1, 2]
//[2, 3]

Array.keys()

1
2
3
4
5
let b = [1,2,3].keys();
for(let i of b){console.log(i);}
//0
//1
//2

Array.values()

1
2
3
4
5
let c = [1,2,3].values();
for(let i of c){console.log(i);}
//1
//2
//3

关于迭代器这个东西,自己说不上什么,因为自己没有亲自用过,如果大家有什么见解课可以评论给我,我来补充和学习一下

4 ES7中的数组方法

4.1 includes()方法

Array.includes方法返回一个布尔值,表示某个数组是否包含给定的值,如果包含,则返回true,否则返回false,与字符串的includes方法类似。

这个方法大家可以看作是ES5中Array.indexOf的语义增强版,“includes”这个是否包含的意思,直接返回Boolean值,比起原来的indexOf是否大于-1,显得更加直观,我就是判断有没有包含哪个值

语法,使用方法和indexof一模一样

1
2
arr.includes(searchElement)
arr.includes(searchElement, fromIndex)

例子

1
2
3
4
5
6
let array = [2, 5, 9];
array.includes(2); // true
array.includes(7); // false
array.includes(9, 2); // true
array.includes(2, -1); // false
array.includes(2, -3); // true


方法还真是tmd多啊,感觉基本上应该是更新完了,前后两星期花了我4天时间吧,还是挺累的。不过收货还是很多,比如知道了ES5的方法基本上都有第二个this指向的参数,重新认识了reduce方法,感觉自己之前很多场景用reduce更好,重新熟悉了一些ES6的方法可以试用有些场景

如果能看到最后的,感觉你也是够累的,哈哈哈。
既然这么累,点颗星吧

理解jquery的深拷贝

发表于 2017-08-24 | 分类于 javascript | 阅读次数
| 字数统计 1,855 | 阅读时长 7

好像自从使用框架之后,对jquery的依赖越来越低了,其好像已经慢慢作为一个工具库的存在了。新项目商量之下,为了减小文件大小,干脆直接不用jquer2,对于一些需要的工具函数直接从jquery提取到一个自己写的工具文件tool.js中。在提取的过程中,也慢慢理解了jquery一些工具函数的源码

深拷贝和浅拷贝的使用场景不同,并没有好坏之分,像对一些基本数据类型,直接可以使用浅拷贝对处理数据。但是对于基本引用类型如嵌套对象,数组(包含着对象的数组),那么就需要使用到深拷贝了。

不想看前面深浅拷贝对比的,可以直接拉到第二章看jquery源码实现


1.浅拷贝解析

原生js也有一些提供拷贝的函数,比如数组的Array.slice(0),Array.concat(),对象的Object.create(),Object.assign()等等,但是都是浅拷贝,遇到二维数组,嵌套对象就通通失败了(以前不懂的时候,真的被坑的不要不要的啊)。

比如下面这个例子,都是在只有基本数据类型的情况下,使用浅拷贝就可以了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
let arr = [1,2,34,5,67,8,9];
let cloneArr = arr.slice(4);
console.log(cloneArr); // [67, 8, 9]
cloneArr[0] = 100; // 修改cloneArr
console.log(arr); // [1,2,34,5,67,8,9],修改cloneArr不影响原数组arr
----------------
let obj = {a : 1,b : 2,c : 3,};
let cloneObj = Object.assign({},obj);// 将拷贝的属性值拷贝到目标对象,然后返回目标对象
console.log(cloneObj); // cloneObj = {a : 1,b : 2,c : 3,};
cloneObj.a = 444; //修改对象
console.log(obj); // obj = {a : 1,b : 2,c : 3,}; 修改拷贝对象不影响源对象

但是如果以上例子将基本数据类型换成引用类型Object和Array呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
let arr = [1,2,{a : 3},{b : 4},5];
let cloneArr = arr.slice(2);
console.log(cloneArr); // [{a : 3},{b : 4},5];
cloneArr[0].a = 100; // 修改cloneArr
console.log(arr); // [1,2,{a : 100},{b : 4},5],修改cloneArr影响原数组arr
----------------
let obj = {a : {aa : 1},b : {bb : 2},c : 3,};
let cloneObj = Object.assign({},obj);// 将拷贝的属性值拷贝到目标对象,然后返回目标对象
console.log(cloneObj); // cloneObj = {a : {aa : 1},b : {bb : 2},c : 3,};
cloneObj.a.aa = 100; //修改对象
console.log(obj); // obj = {a : {aa : 100},b : {bb : 2},c : 3,}; 修改拷贝对象影响到了源对象

为什么会这样子,原因其实也不复杂。js内存分为栈内存和堆内存。所有的基本数据类型都是存储在栈内存中,而引用类型则是存储在堆内存中,提供了一个地址放在了栈内存中。当我们要获取引用类型的值时,先从栈内存获得地址,再根据地址去堆内存中获得值。因此也叫按引用访问。

(去网上浅拷贝了一张图片,因为拷贝了一个图片地址)
去网上浅拷贝了一张图片,拷贝了一个图片地址

而我们上面例子中,每个数组和对象每个属性存储的引用类型obj其实是个地址,我们只是简单的拷贝了属性值,其实就是拷贝了一个地址。所以我们在新对象里进行修改时,由于是通过同一个地址修改了值。因为和原对象共用了一个地址,所以自然就修改了原对象的值了。

2.深拷贝解析

前面解析了浅拷贝。因为我们项目对大型数据处理占据了大头,其中不可避免的会经常用到深拷贝这块。那么深拷贝是怎么实现的。

其实也很简单,就是根据地址找到你堆内存中的值,不断递归深入拷贝下去,直到为基本数据类型为止,接下去就贴上深拷贝代码。

在讲jquey前,还有一个很暴力的方式JSON.parse()和JSON.stringify();缺点是

  • 数据不能包含函数。
  • 如果某个对象属性值为null,会形成一个对象的闭环
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    let obj = {
    a: 1,
    b: 2,
    c: [1,2,3],
    d: function() {
    console.log("asdfghj");
    }
    };
    let result1 = JSON.stringify(target);
    console.log(result1); // 输出结果为"{"a":1,"b":2,"c":[1,2,3]}",函数直接没了
    const obj = {
    foo: {
    name: 'foo',
    bar: {
    name: 'bar'
    baz: {
    name: 'baz',
    aChild: null // 待会将指向obj.bar
    }
    }
    }
    }
    obj.foo.bar.baz.aChild = obj.foo // foo->bar->baz->aChild->foo形成环
    JSON.stringify(obj) // => TypeError: Converting circular personucture to JSON

好了,最后贴上jquery深拷贝的代码和自己一些理解的注释

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
$.fn.extend = function () {
//jquery喜欢在初始定义好所有的变量
let options,// 被拷贝的对象
name,// 遍历时的属性
src,// 返回对象本身的属性值
copy,// 需要拷贝的内容
copyIsArray,// 判断是否为数组
clone,// 返回拷贝的内容
target = arguments[0] || {},
i = 1,
length = arguments.length,
deep = false;// 默认为浅拷贝
//target 是传入的第一个参数,表示是否要深递归
if(typeof target === 'boolean'){
deep = target;
//既然为boolean,则此处初始化target为第二个参数或者空对象
target = arguments[i] || {};
// 如果传了类型为 boolean 的第一个参数,i 则从 2 开始
i ++
}
//如果传入的第一个参数不是对象或者其他,初始化为一个空对象
if(typeof target !== 'object' && $.isFunction(target)){
target = {};
}
//如果只传入了一个参数,表示是jquery静态方法,直接返回自身
if(i === length){
target = this;
i --;
}
for(; i < length; i ++){
if((options = arguments[i]) !== null ){
for( name in options){
src = target[name];//获得源对象的值
copy = options[name];//获得要拷贝对象的值
//说是为了避免无限循环,例如 extend(true, target, {'target':target});
if(target === copy) continue;
//如果是数据正确,且是一个纯粹的对象(纯粹的对象指的是 通过 "{}" 或者 "new Object" 创建的)或者是一个数组的话
if(deep && copy && ($.isPlainObject(copy) || (copyIsArray = Array.isArray(copy)))){
//如果是一个数组的话
if(copyIsArray){
copyIsArray = false;
clone = src && Array.isArray(src) ? src : [];//判断源对象是不是数组,如果不是,直接变为空数组,拷贝属性高优先原则
} else {
clone = src && $.isPlainObject(src) ? src : {};//判断原对象属性是否有值,如果有的话,直接返回原值,否则新建一个空对象
}
//继续深拷贝下去
target[name] = $.extend(deep,clone,copy);
}else if(copy !== undefined){
//如果不为空,则不是需要深拷贝的数据和对象,而是string,data,boolean等等,可以直接赋值
target[name] = copy;
}
}
}
}
// 返回新的拷贝完的对象
return target;
}

在看上段代码中,又发现了几个好方法在业务中会用到的,可以让代码更严谨

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
//判断数据类型
//判断是否为纯正的数据对象
isPlainObject: function( obj ) {
//如果数据不正确,不是对象类型,或者是dom对象,window对象,则直接返回false
if ( !obj || jQuery.type(obj) !== "object" || obj.nodeType || jQuery.isWindow( obj ) ) {
return false;
}
//这段代码是为了兼容IE89存在的,查看是否有constructor属性,如果没有必然是数据对象
try {
if ( obj.constructor &&
!core_hasOwn.call(obj, "constructor") &&
!core_hasOwn.call(obj.constructor.prototype, "isPrototypeOf") ) {
return false;
}
} catch ( e ) {
return false;
}
//对象中key的顺序会将自身属性排在最前面遍历,如果最后一个还是自身属性,则必然所有属性都是自己的
var key;
for ( key in obj ) {}
return key === undefined || core_hasOwn.call( obj, key );
},

以上基本就是jquery.extend代码的解析了。extend是jquery中一个极其重要的方法,jquery本身就用它扩展了许多静态方法和实例方法

promise源码解析(译)

发表于 2017-08-20 | 分类于 javascript | 阅读次数
| 字数统计 4,582 | 阅读时长 18

最新项目中有用promise几个api,对代码结构看起来的确很爽。然后想着去网上找了几篇promise库源码解析的文章。但是看了几篇,感觉还是不能够很理解,然后看到一篇翻译文章有说道q.js库的作者有对promise实现的递进讲解,看了一下,还不错,

Q.js作者源码分析:Q.js作者promise递进讲解实现

网上找的promise源码翻译。文章有些地方翻译的很好,但是对比原文发现少了一些内容,所以读起来很不顺畅。所以自己根据原文也翻译了一遍。对了,本文适合用过promise的人阅读。如果你还没有接触过。可以右转阮一峰的promise讲解


一、极简版异步回调

假如你正在写一个函数不立即返回值函数,需要等待几秒钟后才返回执行结果,你会怎么写呢?思考几秒钟。

最简单的做法自然是写一个回调函数依靠定时器来返回值,比如下面这个

1
2
3
4
5
var oneOneSecondLater = function (callback) {
setTimeout(function () {
callback(1);
}, 1000);
};

这是一个很简单解决问题的方法,但是还有改进的地方,比如能够添加代码执行错误时给出提示。

1
2
3
4
5
6
7
8
9
10
var maybeOneOneSecondLater = function (callback, errback) {
setTimeout(function () {
//进行判断情况,是执行成功的回调,还是执行错误的回调
if (Math.random() < .5) {
callback(1);
} else {
errback(new Error("Can't provide one."));
}
}, 1000);
};

一般的做法是提供一个能同时返回值并且能抛出错误的工具。上面这个例子则演示同时提供回调和错误处理。但是这种写法实在是太定制化了,并不好。

二、Promise基本雏形设计

所以考虑到大多数的情况,代替最简单的返回值和抛出异常,我们更希望函数通常会返回一个对象用来表示最后执行成功或者失败的结果,而这个返回的对象就是promise。从名字上理解,promise表示承诺,那么最终这个promise(承诺)是要被resolve(履行,执行)掉的。

接下去我们开始迭代设计promise。我们先设计一个具有“then”方法的promise模型,通过“then”方法,我们能注册回调函数并且延迟执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
var maybeOneOneSecondLater = function () {
var callback;
setTimeout(function () {
callback(1);
}, 1000);
return {
then: function (_callback) {
callback = _callback;
}
};
};
maybeOneOneSecondLater().then(callback1);

代码写好了。但是大家仔细观察发现该方案仍然还有两个缺点

  • 一是现在方案只能执行一个添加的回调函数。最好的做法是每一个通过then添加进来的回调都能被通知到然后顺序执行。
  • 二是如果这个回调函数是在promise创建好1s之后通过then添加进去,它将无法被调用。

敲黑板,注意注意,接下去开始慢慢搭建promise了。

正常情况下,我们希望可以接收任何数量的回调,且不管是否超时,仍然可以继续注册回调。为了实现这些,我们将创建一个包含两个功能的promise对象。

我们暂时设计了一个defer对象,他的返回值一个包含两部分的对象(这个对象就是promise),一个用来注册观察者(就是”then方法添加回调),一个用来通知所有的观察者执行代码(就是resolve去执行之前添加的所有回调)。

当promise没有被resolve之前,所有回调函数会存储在一个”pengding”的数组中。

当promise被resolve之后,立即执行之前存储的所有回调函数,当回调函数全部执行完毕之后,我们将根据”pengding”来区分状态。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
let defer = () => {
let pending = [],value;
return {
resolve(_value){
value = _value
for(let i = 0;i < pending.length; i++){
pending[i](value)
}
pending = undefined;
},
then(_callback){
if(pending){
pending.push(_callback)
}else{
_callback();
}
}
}
}
let oneOneSecondLater = () => {
let result = defer();
setTimeout(()=> {
result.resolve(1);
}, 1000);
return result;
};
oneOneSecondLater().then(callback);

这开始的第一步很关键啊,因为此时我们已经可以做到

  1. 可以任意时间添加任意多的回调;
  2. 可以人为决定什么时候resolve;
  3. 当promise被resolve之后,还可以添加回调,只不过此时立即就执行了

但是还有一些问题,比如

  1. defer可以被resolve执行多次,我们并没有给出一个错误的提示。而且事实上为了避免恶意或者无意的不断去resolve,我们仅允许第一次调用可以通知回调并执行。
  2. 添加回调只能通过defer.then添加,不能链式调用,即defer.then(callback).then(callback)

那么接下来我们先修正第一个问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
let defer = () => {
let pending = [],value
return {
resolve(_value){
if(pending){
value = _value
for(let i = 0;i < pending.length; i++){
pending[i](value)
}
pending = undefined;
}else{
throw new Error("A promise can only be resolved once.")
}
},
then(_callback){
if(pending){
pending.push(_callback)
}else{
_callback();
}
}
}
}

好,现在我们已经保证不能重复defer.resolve()的问题了,那么我们还希望可以实现通过链式调用来添加回调。可是目前要只能通过defer().then(callback1),defer().then(callback2),defer().then(callback3)这种方式添加回调,这显然不是我们想要的方式。接下来我们将一步一步实现。

三、promise职责分离

但是在实现链式回调之前,为了后期结构,我们希望对我们的promise进行职责区分,一个注册观察者,一个执行观察者。根据最少授权原则,我们希望如果授权给某人一个promise,这里只允许他增加观察者;如果授权给某人resolver,他应当仅仅能决定什么时候给出解决方案。因为大量实验表明任何任何不可避免的越权行为会导致后续的改动变得很难维护。(其实就是希望把添加回调的then功能移植到promise中,从defer.then转变成defer.promise.then,保证功能的纯粹性)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
let defer = () => {
let pending = [],value;
return {
resolve(_value){
if(pending){
value = _value
for(let i = 0;i < pending.length; i++){
pending[i](value)
}
pending = undefined;
}else{
throw new Error("A promise can only be resolved once.")
}
},
promise: {
then (callback) {
if (pending) {
pending.push(callback);
} else {
callback(value);
}
}
}
}
}

当职责分离完之后,我们就可以接下去实现一步关键的改造

四、promise的链式调用

上文说道要实现链式回调,我们首先要能在下一个回调函数里接受上一个回调的值。依靠上一步的职责分离的基础,我们接下来要跨非常大的一步,就是使用旧的promise去驱动新的promise。我们希望通过promise组合的使用,来实现值的传递。

举个例子,让你写一个相加的函数,接受两个回调函数返回的数字相加。大家可以考虑如何实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var twoOneSecondLater = function (callback) {
var a, b;
var consider = function () {
if (a === undefined || b === undefined)return;
callback(a + b);
};
oneOneSecondLater(function (_a) {
a = _a;
consider();
});
oneOneSecondLater(function (_b) {
b = _b;
consider();
});
};
twoOneSecondLater(function (c) {
// c === 2
});

上面这个方法虽然做到了,但是这个方法是脆弱的,因为我们在执行相加函数时,需要额外的代码去判断相加的数字是否有效。

于是我们希望用更少的代码去实现上面的需求,比如就像下面这样

1
2
3
4
5
6
7
8
//上面的函数如果用更少的步骤来表达就是
var a = oneOneSecondLater();
var b = oneOneSecondLater();
var c = a.then(function (a) {
return b.then(function (b) {
return a + b;
});
});

上面这个例子其实想表达的就是实现callback返回值的传递,如callback1的返回值传给callback2,将callback2的返回值传给callback3。
为了实现上面例子的这种效果,我们要实现以下几点

  • 每个then方法后必须要返回一个promise
  • 每一个promise被resolve后,返回的必然是一个新的promise或者是一个执行过的值
  • 返回的promise最终可以带着回调的值被resolve掉(这句话有点难翻译,感觉就是promise.resolve(_value));

我们实现一个函数可以将获得的值传给下一个回调使用

1
2
3
4
5
6
7
let ref = (value) => {
return {
then(callback){
callback(value);
}
}
}

不过考虑到有时候返回的值不仅仅是一个值,而且还可能是一个promise函数,所以我们需要加个判断

1
2
3
4
5
6
7
8
9
10
let ref = (value) => {
if(value && typeof value.then === "function"){
return value;
}
return {
then(callback){
callback(value);
}
}
}

这样子我们在使用中就不需要考虑传入的值是一个普通值还是一个promise了。

接下来,为了能使then方法也能返回一个promise,我们来改造下then方法;我们强制将callback的返回值传入下一个promise并立即返回。
这个例子存储了回调的值,并在下一个回调中执行了。但是上面第三点没有实现,因为返回值可能是一个promise,那么我们继续改进一下方法

1
2
3
4
5
6
7
8
9
10
let ref = (value) => {
if(value && typeof value.then === "function"){
return value;
}
return {
then(callback){
return ref(callback(value));
}
}
}

通过这一步增强之后,基本上就可以做到获得上一个回调值并不断链式调用下去了。

接下去我们考虑到一种比较复杂的情况,就是defer中存储的回调会在未来某个时间调用。于是我们需要在defer里面将回调进行一次封装,我们将回调中执行完后通过then方法去驱动下一个promise并传递一个返回值。

此外,resolve方法应该能处理本身是一个promise的情况,resolve可以将值传递给promise。因为不管是ref还是defer都可以返回一个then方法。如果promise是ref类型的,将会通过then(callback)立即执行回调。如果是promise是defer类型的,callback暂时被存储起来,依靠下一个then(callback)调用才能执行;所以变成了callback可以监听一个新的promise以便能获取完全执行后的value。

根据以上要求,得出了下面最终版的promise

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
let isPromise = (value) => {
return value && typeof value.then === "function";
};
let ref = (value) => {
if (value && typeof value.then === "function")
return value;
return {
then (callback) {
return ref(callback(value));
}
};
};
let defer = () => {
let pending = [], value;
return {
resolve: function (_value) {
if (pending) {
value = ref(_value); // values wrapped in a promise
for (let i = 0, ii = pending.length; i < ii; i++) {
let callback = pending[i];
value.then(callback); // then called instead
}
pending = undefined;
}
},
promise: {
then: function (_callback) {
let result = defer();
// callback is wrapped so that its return
// value is captured and used to resolve the promise
// that "then" returns
let callback = function (value) {
result.resolve(_callback(value));
};
if (pending) {
pending.push(callback);
} else {
value.then(callback);
}
return result.promise;
}
}
};
};
let a = defer();
a.promise.then(function(value){console.log(value);return 2}).then(function(value){console.log(value)});
a.resolve(1);

将defer分为两个部分,一个是promise,一个是resolve

到了这一步基本上的promise功能已经实现了,可以链式调用,可以在自己控制在未来某个时间resolve。接下去就是功能的增强和补足了。

这一块回调基本上就写完了,看了很久原文的描述,对着代码理解作者想表达的意思。不过英语不太好,写的磕磕绊绊。╮(╯▽╰)╭,感觉还是有些地方写的不对。希望有人能够纠错出来。

五、提供错误的回调

为了实现错误消息的传递,我们还需要一个错误的回调函数(errback)。就像promise完全执行时调用callback一样,它会告知执行errback以及告诉我们拒绝的原因。

实现一个类似于前面ref的函数。

1
2
3
4
5
6
7
let reject = (reason) => {
return {
then(callback,errback){
return ref(errback(reason);
}
}
}

最简单的实现方法是当监听到返回值时,立即执行代码

1
2
3
reject("Meh.").then((value) => {},(reason) => {
throw new Error(reason);
}

那么接下来我们改进原来promsie这个API,引入“errback”。

为了将错误回调添加到代码中,defer需要添加一种新的容器来添加成功回调和错误回调。因此之前那个存储在数组(pending)中的只有一种待处理回调函数,我们需要重新设计一个同时包含成功回调和错误回调的数组([callback,errback]),根据then传入的参数决定调用哪个。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
var defer = function () {
var pending = [], value;
return {
resolve: function (_value) {
if (pending) {
value = ref(_value);
for (var i = 0, ii = pending.length; i < ii; i++) {
// apply the pending arguments to "then"
value.then.apply(value, pending[i]);
}
pending = undefined;
}
},
promise: {
then: function (_callback, _errback) {
var result = defer();
var callback = function (value) {
result.resolve(_callback(value));
};
var errback = function (reason) {
result.resolve(_errback(reason));
};
if (pending) {
pending.push([callback, errback]);
} else {
value.then(callback, errback);
}
return result.promise;
}
}
};
};
let ref = (value) => {
if (value && typeof value.then === "function")
return value;
return {
then: function (callback) {
return ref(callback(value));
}
};
};
let reject = (reason) => {
return {
then: function (callback, errback) {
return ref(errback(reason));
}
};
};

代码写完了,但是仍然还有地方可以改进。

比如作者说到这一步有一个问题,就是如果按照上面这么写,那么所有的then函数就必须提供错误回调函数(_errback),如果不提供就会出错。所以最简单的解决方法是提供一个默认的回调函数。甚至文中还说,如果仅仅是对错误回调有需要,那么忽略不写成功回调(_callback)也是可以的。所以为了满足需求,我们为_callback和_errback都提供一个默认的回调函数。(好吧,其实我就是觉得这是一个好的库的容错处理)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
var defer = function () {
...
return{
...
promise : {
then: function (_callback, _errback) {
var result = defer();
// 提供一个默认的成功回调和错误回调
_callback = _callback || function (value) {
// 默认执行
return value;
};
_errback = _errback || function (reason) {
// 默认拒绝
return reject(reason);
};
var callback = function (value) {
result.resolve(_callback(value));
};
var errback = function (reason) {
result.resolve(_errback(reason));
};
if (pending) {
pending.push([callback, errback]);
} else {
value.then(callback, errback);
}
return result.promise;
}
}
}
}
}

好了,现在我们已经实现了接收构造或者隐含的错误回调这一步的完成版

六、安全性和稳定性

我们还有需要需要提高的地方就是要保证callbacks和errbacks在未来他们被调用的时候,应该是和注册时的顺序是保持一致的。这将显著降低异步编程中流程控制出错可能性。文中举了一个有趣的小例子.

1
2
3
4
5
6
7
8
9
var blah = function () {
var result = foob().then(function () {
return barf();
});
var barf = function () {
return 10;
};
return result;
};

上面这个函数在执行后会出现两种情况,一是抛出一个异常,二是顺利执行并返回了值10。而决定是哪个结果的是foob()是否在正确顺序里。因为我们希望哪怕回调在未来被延迟执行了,它能够执行成功。

下面添加了一个enqueue方法,我的理解就是依靠setTimeout的异步将所有回调按照顺序添加到任务队列中,保证按照顺序执行代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
let enqueue = (callback) => {
setTimeout(callback,1)
}
let enqueue = (callback) => {
//process.nextTick(callback); // NodeJS
setTimeout(callback, 1); // Naïve browser solution
};
let defer = function () {
let pending = [], value;
return {
resolve: function (_value) {
if (pending) {
value = ref(_value);
for (let i = 0, ii = pending.length; i < ii; i++) {
enqueue(function () {
value.then.apply(value, pending[i]);
});
}
pending = undefined;
}
},
promise: {
then: function (_callback, _errback) {
let result = defer();
_callback = _callback || function (value) {
return value;
};
_errback = _errback || function (reason) {
return reject(reason);
};
let callback = function (value) {
result.resolve(_callback(value));
};
let errback = function (reason) {
result.resolve(_errback(reason));
};
if (pending) {
pending.push([callback, errback]);
} else {
// XXX
enqueue(function () {
value.then(callback, errback);
});
}
return result.promise;
}
}
};
};
let ref = function (value) {
if (value && value.then)
return value;
return {
then: function (callback) {
let result = defer();
// XXX
enqueue(function () {
result.resolve(callback(value));
});
return result.promise;
}
};
};
let reject = function (reason) {
return {
then: function (callback, errback) {
var result = defer();
// XXX
enqueue(function () {
result.resolve(errback(reason));
});
return result.promise;
}
};
};

虽然将需要的回调依照次序添加到了队列中

作者有考虑到一些新的问题,比如

  • callback或者errback必须以同样的顺序被调用
  • callback或者errback可能会被同时调用
  • callback或者errback可能会被调用多次

于是我们需要找个机会then的回调函数,为了保证当回调函数中程序出错时,可以转入到报错函数中。(其实又是一个库的容错处理,保证代码出错时不中断程序的执行)。

用when方法封装下promise以此阻止错误发生,确保不会有哪些突发性的错误,包括哪些非必需的事件流控制,并且也能使callback和errback各自保持独立。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
var when = function (value, _callback, _errback) {
var result = defer();
var done;
_callback = _callback || function (value) {
return value;
};
_errback = _errback || function (reason) {
return reject(reason);
};
var callback = function (value) {
try {
return _callback(value);
} catch (reason) {
return reject(reason);
}
};
var errback = function (reason) {
try {
return _errback(reason);
} catch (reason) {
return reject(reason);
}
};
enqueue(function () {
ref(value).then(function (value) {
if (done)
return;
done = true;
result.resolve(ref(value).then(callback, errback));
}, function (reason) {
if (done)
return;
done = true;
result.resolve(errback(reason));
});
});
return result.promise;
};

这一步的完整版

六、消息传递

现在这一步来看,promise已经成为了一个具有接受消息功能的类了。Deferred promise根据获得的消息来执行对应的回调函数,返回对应的值。当你接收到完全成功执行的值,则在then中执行成功的回调函数返回msg;获得错误的值则在then中执行错误回调函数,返回错误的原因

因此我们基本可以认为promise这个类可以接受任何的值,包括”then/when”这些信息。这对于一些非立即执行函数的监听非常有用。举个例子,当你发了一个网络请求,等待返回值才能执行函数。我们等待这个请求的往返的过程中浪费了许多时间,而promise仿佛在电脑中另外开了一个线程进行监听这些返回值,然后执行对应的回调函数(这个例子是自己理解举的,非原文,如有不对,欢迎改正)。

翻到这里有点崩溃了,捂下脑子,接下去感觉有点头疼了,以后再补吧,因为基本形态的promise已经出来。接下去是另外一种需求的promise了

接下来我们要包装一种新型的promise,这套promise基于一些能发送任意消息的方法之上,可以满足 “get”, “put”, “post”能发送相应的消息,并且能根据返回结果中执行相应的promise。


第一次尝试翻译,真的是个体力活,花了快2天的时间,整个人都是炸的。不过所幸是比以前明白了一些恭喜。
原文大概讲解了基本的promise构成,但是现在还是有许多方法并没有分析,接下去我按照自己的想法去实现以下promise.all方法。如果写的不好,欢迎大家指正,帮我进步一下,谢谢。(手动捂脸)

第十六章-站在巨人的肩膀

发表于 2017-08-14 | 分类于 《解释的工具-生活中的经济学原理》 | 阅读次数
| 字数统计 933 | 阅读时长 3

《解释的工具-生活中的经济学原理》

—–第十六章-站在巨人的肩膀

这章也是本书的最后一章了,看到这里,我不禁问自己两个问题,1.站在经济学的角度,你是否有培养出抽象思考的能力,面对社会的各种现象,是否有一套自己的理论而不是依然人云亦云。2.站在了巨人肩膀上,你是否理解了经济学的思维呢。

第一章节对于人性的分析即生活经济学,让我明白了我们生活中很多不知不觉的习惯其实就是经济学思维的影响,理性与自利的人性,懒惰的天性,信任的价值都是生活中的经济哲学。

第二章的社会经济学则解释了许多社会想象的产生,冷漠的围观者,无情的竞争,残忍的管制都可以由经济学的成本分析。幸运的是,我还相信这中国会变得原来越好,而不是我再网上看到各种黑暗的地方。

第三章政治经济学解释了许多国家政策产生的原因以及我们普通人在这些政策之下受到的影响。无奈的是,虽然我明白了这些政策的最终方向,可是却无法改变自己在大潮下的走势。逆水行舟,不进则退啊。

第四章的法律经济学有时候看得我心惊胆战,世界上是否存在真正的公平与正义,你所看到的真的对的,有价值,有意义的吗?面对真正的选择,跳出自己所在的层面,站在更多更高的角度,也许会发现更多之前没有考虑到的,然后做出的取舍,才显得更加的“公平与正义”。

纵览全书,又人及社会及政治及法律,经济学有由小到大,由点及面,细细解析其所影响的各个层面。归纳出最后4点。

1. 人是理性的,自私的。

人们是会思索判断的生物,大多数行为总是为了增添自己(或者自己周围人的福祉)。引申出来的许多懒惰的特性也是由此而来。

2. 存在不一定合理,但是存在一定是有原因的。

正是因为行为都是基于理性和自私,所以近年来出现许多冷漠的社会现象(冷眼围观,碰瓷,污蔑)。这些社会现象的出现,都有背后条件的支持————存在不一定合理,但是存在一定是有原因的。

3. 好的价值要出现,是有条件的

每个人都希望自己有好的环境,事业上有好的发展,可是这必须你做出一些努力,父母朋友间的互动,自身努力获得好的经济等等。个人小的价值尚且需要这些努力,更大的价值当然需要更困难的条件来支持。所以说,好的价值的出现,是有条件的

4. 一件事物的意义,是由其他事物衬托出来的。

最后要说的是我们生活中许多隐含经济学的经验和原则,都是你所在相对环境形成的。如果环境发生改变,则你自己原先所认定的原则自然也会随之发生变化(你要学会拥抱变化)。因此事物的意义,是相对于环境里的各种主管条件,是相对的而不是绝对的。

观点在不断的变化,我们不断的在成长。

基于vue-cli的webpack优化之路

发表于 2017-08-13 | 分类于 webpack | 阅读次数
| 字数统计 1,990 | 阅读时长 8

最近的项目度过了开始忙碌的基建期,也慢慢轻松下来,准备记录一下自己最近webpack优化的措施,希望有温故知新的效果。

项目采用的是vue全家桶,构建配置都是基于vue-cli去改进的。关于原始webpack配置大家可以看下这篇文章vue-cli#2.0 webpack配置分析,文章基本对于文件每行代码都做了详细的解释,有助于更好的理解webpack。
项目位置链接

仔细总结了一下,自己的优化基本还是网上流传的那几点

  • 通过 externals 配置来提取常用库,引用cdn
  • 合理配置CommonsChunkPlugin
  • 善用alias
  • dllplugin启用预编译
  • happypack多核构建项目

不过经过自己的实践最后三点是对自己项目优化最大的。文章也主要对后面几点详细说明一下

对了,我项目引用了vue全家桶一套,jquery以及两个第三方插件,element-ui,echarts,自己项目的组件大概有40个左右

原来打包一个项目所需要的时间基本在35-40秒左右(第二次有缓存会稍微快一点),但是偶尔来一次大姨妈,时间甚至要到50s左右,我也是醉了。不过大家可以期待一下经过下面这三步优化大概需要多久。

1.使用dllplugin预编译与引用

首先为什么要引用Dll?在网上浏览了一些文章后,我发现上除了加快构建速度以外,使用webpack的dll还有一个好处。

Dll打包以后是独立存在的,只要其包含的库没有增减、升级,hash也不会变化,因此线上的dll代码不需要随着版本发布频繁更新。 因为使用Dll打包的基本上都是独立库文件,这类文件有一个特性就是变化不大。当我们正常打包这些库文件到一个app.js里的时候,由于其他业务文件的改变,影响了缓存对构建的优化,导致每次都要重新去npm包里寻找相关文件。而使用了DLL之后,只要包含的库没有升级, 增减,就不需要重新打包。这样也提高了构建速度。

那么如何使用Dll去优化项目呢
首先要建立一个dll的配置文件,引入项目所需要的第三方库。这类库的特点是不需要随着版本发布频繁更新,长期稳定。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const webpack = require('webpack');
const path = require('path');
module.exports = {
entry: {
//你需要引入的第三方库文件
vendor: ['vue','vuex','vue-router','element-ui','axios','echarts/lib/echarts','echarts/lib/chart/bar','echarts/lib/chart/line','echarts/lib/chart/pie',
'echarts/lib/component/tooltip','echarts/lib/component/title','echarts/lib/component/legend','echarts/lib/component/dataZoom','echarts/lib/component/toolbox'],
},
output: {
path: path.join(__dirname, 'dist-[hash]'),
filename: '[name].js',
library: '[name]',
},
plugins: [
new webpack.DllPlugin({
path: path.join(__dirname, 'dll', '[name]-manifest.json'),
filename: '[name].js',
name: '[name]',
}),
]
};

基本配置参数和webpack基本一模一样,相信来看优化的都明白什么意思,我就不解释了。然后执行代码编译文件。(我的配置文件是放在build里面,下方路径根据项目路径需要变动)

1
webpack -p --progress --config build/webpack.dll.config.js

当运行完执行后,会生成两个新文件在目录同级,一个是生成在dist文件夹下的verdor.js,里面是刚刚入口依赖被压缩后的代码;一个是dll文件夹下的verdor-manifest.json,将每个库进行了编号索引,并且使用的是id而不是name。

接下去你只要去你的webpack配置文件的里的plugin中添加一行代码就ok了。

1
2
3
4
5
6
7
8
9
const manifest = require('./dll/vendor-manifest.json');
...
...,
plugin:[
new webpack.DllReferencePlugin({
context: __dirname,
manifest,
}),
]

这时候再执行webpack命令,可以发现时间直接从40秒锐减到了18-20s左右,整整快了一倍有木有(不知道是不是因为自己依赖库太多了才这样的,手动捂脸)。

2.happypack多线程编译

一般node.js是单线程执行编译,而happypack则是启动node的多线程进行构建,大大提高了构建速度。使用方法也比较简单。以我项目为例,在插件中new一个新的happypack进程出来,然后再使用使用loader的地方替换成对应的id

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
var HappyPack = require('happypack');
...
...
modules:{
rules : [
...
{
test: /\.js$/,
loader:[ 'happypack/loader?id=happybabel'],
include: [resolve('src')]
},
...
]
},
...
...
plugin:[
//happypack对对 url-loader,vue-loader 和 file-loader 支持度有限,会有报错,有坑。。。
new HappyPack({
id: 'happybabel',
loaders: ['babel-loader'],
threads: 4,//HappyPack 使用多少子进程来进行编译
}),
new HappyPack({
id: 'scss',
threads: 4,
loaders: [
'style-loader',
'css-loader',
'sass-loader',
],
})
]

这时候再去执行编译webpack的代码,打印出来的console则变成了另外一种提示。而编译时间大概从20s优化到了15s左右(感觉好像没有网上说的那么大,不知道是不是因为本身js比重占据太大的缘故)。

3.配合resolve,善用alias

本来是没有第三点的,只不过在搜索网上webpack优化相关文章的时候,看到用人提到把引入文件改成库提供的文件(原理我理解其实就是1.先通过resolve指定文件寻找位置,减小搜索范围;2.直接根据alias找到库提供的文件位置)。

vue-cli配置文件中提示也有提到这一点,就是下面这段代码

1
2
3
4
5
6
7
8
9
resolve: {
//自动扩展文件后缀名,意味着我们require模块可以省略不写后缀名
extensions: ['.js', '.vue', '.json'],
//模块别名定义,方便后续直接引用别名,无须多写长长的地址
alias: {
'vue$': 'vue/dist/vue.esm.js',//就是这行代码,提供你直接引用文件
'@': resolve('src'),
}
},

然后我将其他所有地方关于vue的引用都替换成了vue$之后,比如

1
2
// import 'vue';
import 'vue/dist/vue.esm.js';

时间竟然到了12s,也是把我吓了一跳。。。

然后我就把jquery,axios,vuex等等全部给替换掉了。。。不过变化没有特别大,大概优化到了11s左右,美滋滋,O(∩_∩)O~~。如果有缓存的情况下,基本上大概在9s左右

4.webpack3升级

本来是没第四点,刚刚看到公众号推出来一篇文章讲到升级到webpack3的一些新优点,比如Scope Hoisting(webpack2升级到webpack3基本上没有太大问题)。通过添加一个新的插件

1
2
3
4
5
// 2017-08-13配合最新升级的webpack3提供的新功能,可以使压缩的代码更小,运行更快
...
plugin : [
new webpack.optimize.ModuleConcatenationPlugin(),
]

不过在添加这行代码之后,构建时间并没有太大变化。因为它的优点是提供js在浏览器中的运行速度。webpack2会把每个处理后的模块用一个函数包裹起来,导致浏览器中的JS执行效率降低,主要是因为闭包函数降低了JS引擎解析速度。

不过在浏览器中国的实际效果感觉不出来太大差别

然后还有一个是webpack3中所有的模块支持用ID进行标记,如果重复引用相同的模块

5.去除不必要的文件

因为要引入代码高亮的highlight.js插件,webpack会引入里面有各个语言的js文件,但是我们项目只需要js,html,css。搜了一下发现网上已经有类似的解决方法了,ContextReplacementPlugin会根据你写的正则去匹配你需要的文件。

而且自己记得webpack3的升级中有个新特性tree shaking就是可以从文件树中去除不必要的文件。

好了基本上感觉就是以上这些效果对项目的优化最大,虽然没有到网上说的那种只要3~4秒时间那么变态,不过感觉基本9-12秒的时间也可以了。

重温vue双向绑定原理

发表于 2017-08-12 | 分类于 vue | 阅读次数
| 字数统计 2,224 | 阅读时长 9

摘要:因为项目刚开始用的vue框架,所以早期也研究了一下他的代码看过相关文章的解析,说说也能说个七七八八。不过今天再去看以前的demo的时候,发现忽然一知半解了,说明当时可能也没有理解透,所以写篇文章让自己理解的更深一些。

本篇文章大多数知识点实在学习了这篇Vue.js双向绑定的实现原理之后避免遗忘,所以写这个温故知新,加强理解。

项目位置链接


一、访问器属性

如果稍微看过相关文章的人都知道vue的实现是依靠Object.defineproperty()来实现的。每个对象都有自己内置的set和get方法,当每次使用set时,去改变引用该属性的地方,从而实现数据的双向绑定。简单举例

1
2
3
4
5
6
7
8
9
10
11
const obj = {};
Object.defineProperty(obj,'hello',{
get(value){
console.log("啦啦啦,方法被调用了");
},
set(newVal,oldVal){
console.log("set方法被调用了,新的值为" + newVal)
}
})
obj.hello; //get方法被调用了
obj.hello = "1234"; //set方法被调用了

二、极简双向绑定的实现

基于这个原理,如果想实现显示文字根据输入input变化,实现一个简单版的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<input type="text" id="a"/>
<span id="b"></span>
<script>
const obj = {};
Object.defineProperty(obj,'hello',{
get(){
console.log("啦啦啦,方法被调用了");
},
set(newVal){
document.getElementById('a').value = newVal;
document.getElementById('b').innerHTML = newVal;
}
})
document.addEventListener('keyup',function(e){
obj.hello = e.target.value;
})
</script>

上面这个实例实现的效果是:随着文本框输入文字的变化,span会同步显示相同的文字内容。同时在控制台用js改变obj.hello,视图也会更新。这样就实现了view->model,model->view的双向绑定。

三、拆解任务,实现vue的双向数据绑定

我们最终实现下面vue的效果

1
2
3
4
5
6
7
8
9
10
11
<div id="app">
<input type="text" v-model="text"/>
</div>
<script>
const vm = new Vue({
id : "app",
data : {
text : "hello world"
}
})
</script>

1.输入框的文本与文本节点的data数据绑定
2.输入框的内容发生变化时,data中的数据也发生变化,实现view->model的变化
3.data中的数据发生变化时,文本节点的内容同步发生变化,实现model->view的变化

要实现1的要求,则又涉及到了dom的编译,其中有一个DocumentFragment的知识点。

四、DocumentFragment

众所周知,vue吸收了react虚拟DOM的优点,使用DocumentFragment处理节点,其性能和速度远胜于直接操作dom。vue进行编译时,就是将所有挂载在dom上的子节点进行劫持到使用DocumentFragment处理节点,等到所有操作都执行完毕,将DocumentFragment再一模一样返回到挂载的目标上。

先实现一段劫持函数,将要操作的dom全部劫持到DocumentFragment中,然后再append会原位置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<div id="app">
<input type="text" v-model="text"/>
</div>
<script>
const app = document.getElementById("app");
const nodetoFragment = (node) => {
const flag = document.createDocumentFragment();
let child;
whild(child = node.firstChild){
flag.appendChild(child);//不断劫持挂载元素下的所有dom节点到创建的DocumentFragment
}
return flag
}
const dom = nodetoFragment(app);
</script>

五、数据初始化绑定

当已经获取到所有的dom元素之后,则需要对数据进行初始化绑定,这里简单涉及到了模板的编译。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
// 编译HTML模板
const compile = (node,vm) => {
const regex = /\{\{(.*)\}\}/;//为临时正则表达式,为demo而生
//如果节点类型为元素的话
if(node.nodeType === 1){
const attrs = node.attributes;//学到一个新属性。。。
for(let i = 0;i < attrs.length; i++){
let attr = attrs[i];
if(attr.nodeName === "v-model"){
let name = attr.nodeValue;
node.addEventListener("input",function (e) {
vm.data[name] = e.target.value;
})
node.value = vm.data[name];
node.removeAttribute("v-model");
}
}
}
//如果节点类型为文本的话
if(node.nodeType === 3){
if(regex.test(node.nodeValue)){
let name = RegExp.$1;//获取搭配匹配的字符串,又学到了。。。
name = name.trim();
node.nodeValue = vm.data[name];
}
}
};
//劫持挂载元素到虚拟dom
let nodeToFragment = (node,vm) => {
const flag = document.createDocumentFragment();
let child;
while(child = node.firstChild){
compile(child,vm);//绑定数据,插入到虚拟DOM中
flag.appendChild(child);
}
return flag;
};
//初始化
class Vue {
constructor(option){
this.data = option.data;
let id = option.el;
let dom = nodeToFragment(document.getElementById(id),this);
document.getElementById(id).appendChild(dom);
}
}
const vm = new Vue({
el : "app",
data : {
text : "hello world"
}
})

通过以上代码先实现了第一个要求,文本框和文本节点已经出现了hello woeld了

六、响应式的数据绑定

接下来我们要实现数据双向绑定的第一步,即view->model的绑定。根据之前那个简单的例子看到,我们实时获取input中的值,通过Object.defineProperty将data中的text设置为vm的访问器属性,通过set方法,当我们在设置vm.data的值时,实现数据层的绑定。在这一步,set中要做的操作是更新属性的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
let defineReactive = (obj,key,val) => {
Object.defineProperty(obj,key,{
get(val){
return val;
}
set(newVal,oldVal){
if(newVal === oldVal) return;
val = newVal;
console.log(val);
}
})
};
//监听数据
let observe = (obj,vm) => {
Object.keys(obj).forEach((key)=>{
defineReactive(vm.data,key,obj[key]);
})
};

七、订阅/发布模式(subscribe&publish)

text 属性变化了,set方法触发了,可以通过view层的改变实时改变数据,可是并没有改变文本节点的数据。一个新的知识点:订阅发布模式。

订阅发布模式(又称为观察者模式)定义了一种一对多的关系,让多个观察者同时监听一个主题对象,这个主体对象的改变会通知所有观察者对象。

发布者发出通知=>主题对象收到通知并推送给订阅者=>订阅者执行操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 三个订阅者
let sub1 = {updata(){console.log(1);}};
let sub2 = {updata(){console.log(2);}};
let sub3 = {updata(){console.log(3);}};
// 一个主题发布器
class Dep{
constructor(){
this.subs = [sub1,sub2,sub3];
}
notify(){
subs.forEach((sub) => {
sub.updata();
})
}
}
const dep = new Dep();
// 一个发布者
const pub = {
publish(){
dep.notipy();
}
};
pub.publish();

上图为一个简单实例,发布者执行发布命令,所有这个主题的订阅者执行更新操作。接下去我们要做的就是,当set方法触发后,input作为发布者,改变了text属性;而文本节点作为订阅者,在收到消息后执行更新操作。

八、双向绑定的实现

每次new一个新的vue对象时,主要是做了两件事,一件是监听数据:observer(监听数据),第二个是编译HTML,nodeToFragement(id)。

在监听数据的过程中,会为data中的每一个属性生成一个主题对象。

而在编译HTML的过程中,会为每个与数据绑定的相关节点生成一个订阅者watcher,订阅者watcher会将自己订阅到相应属性的dep中。

在前面的方法中已经实现了:修改输入框内容=>再时间回调中修改属性值=>触发属性的set方法。

接下来要做的是发出通知dep.notify=>发出订阅者的uodate方法=>更新视图。

那么如何将watcher添加到关联属性的dep中呢。

编译HTML过程中,为每一个与data关联的节点生成一个watcher,那么watcher中又发生了什么?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 每一个属性节点的watcher
class Watcher{
constructor(vm,node,name){
Dep.target = this;
this.name = name;
this.node = node;
this.vm = vm;
this.update();
Dep.target = null;
}
update(){
//获得最新值,然后更新视图
this.get();
this.node.nodeValue = this.value;
}
get(){
this.value = this.vm.data[this.name];
}
}

在编译HTML的过程中,生成watcher

1
2
3
4
5
6
7
8
9
10
11
12
13
let complie = (node,vm){
......
//如果节点类型为文本的话
if(node.nodeType === 3){
if(regex.test(node.nodeValue)){
let name = RegExp.$1;
name = name.trim();
node.nodeValue = vm.data[name];
new Watcher(vm,node,name);//在此处添加订阅者
}
}
}

首先将自己赋给了一个全局变量Dep.target;然后执行了uodate方法,进而执行了get方法,读取了vm的访问器属性,从而触发了访问器属性的get方法,get方法将相应的watcher添加到对应访问器属性的dep中。再次,获取属性的值,然后更新视图。最后将dep.target设置为空,是因为这是个全局变量也是watcher与dep之间唯一的桥梁,任何时间都只能保证只有一个值。(其实就是说全局一个主题,每个订阅者和发布者都是通过这个主题进行沟通。当执行代码时,这个主题接受到一个发布通知,通知完所有订阅者,然后注销掉,用于下一个通知发布。啰嗦了一段就是想讲为什么要设置Dep.target = null)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 一个主题发布器
class Dep(){
constructor(){
this.subs = [];
}
notify(){
this.subs.forEach((sub) => {
sub.update();
}
}
addSub(sub){
this.subs.push(sub);
}
}
let defineReactive = (obj,key,val) => {
let dep = new Dep();
Object.defineProperty(obk,key,{
get(){
if(dep.target) dep.addSub(dep.target);
}
set(newVal,oldVal){
if(newVal === oldVal) return;
val = newVal;
dep.notify();
}
})
}

至此,hello world 双向绑定就基本实现了。文本内容会随输入框内容同步变化,在控制器中修改 vm.text 的值,会同步反映到文本内容中。

第十五章-以管窥天

发表于 2017-08-07 | 分类于 《解释的工具-生活中的经济学原理》 | 阅读次数
| 字数统计 582 | 阅读时长 2

《解释的工具-生活中的经济学原理》

—–第十五章-以管窥天?

工作也慢慢不忙了,生活也慢慢恢复原样了,回到了原来的轨道上面去了。人们的在思考与做每一个决定都是有他的利弊,当下做出的选择。比起做决定,更大的收货是你在做决定时的思维模式。不管你最后的结果怎么样,当你在面对各种现象和问题时,有自己的认知和思索方式,便是你独有的财富。

本章主要想讲述的其实也是这种思维模式的重要性。我们一生中会做出很多选择,你自己有时候也不知道自己做的选择是否正确,就像自己最近感情上做出的选择,你放弃了很多的同时,也会有相应许多的得到的,也许再过了一段时间之后,你会后悔或者遗憾当时你做的选择,但是这就是你在当时情境下自己做的最好的决定。很多事情有两个选项给你选已经是很幸福的事情了。

经济学在生活中的很多表现其实也就是帮我们做出取舍。看电影,吃饭,睡觉等许多事情不仅仅是由事情本身所决定的,更多会受到周围工作,场景,同事等因素影响。经济学擅长将各个因素抽象化,图表化在心中列出来。自己在思考问题时,可以明确,精致,有效的掌握每一个环节,从而归纳出有意义的结果。也许也会在思考对比的过程中,想出更好的替代方案。

这篇看完后其实没有太多可以分析,但是细细思考,最重要的早已在生活中潜移默化的影响了我们。以后在做每个决定时,独立全面的思考决定所带来的利弊,也许几年后在后悔自己sb的举动,但是至少你曾经细致的去想过每个选项,在你当时的情境下做出最好的选择。并且如果你真的当你思考全面当作出后,也不要再纠结了。

第十四章-司法女神的举止

发表于 2017-07-27 | 分类于 《解释的工具-生活中的经济学原理》 | 阅读次数
| 字数统计 879 | 阅读时长 3

《解释的工具-生活中的经济学原理》

—–第十四章-司法女神的举止

好久好久没有看书了,一是因为最近的项目是在是太紧张了,要赶在deadline前完成,同时手贱半个月前空的时候接了个私活,更加忙的不可开交。好不容易等忙完了,好了,吃了个特辣的香锅整个人身体垮了。也算明白了一个道理,欠的账总是要还的。你加班忙碌期的疲惫会在你松懈下的那一刻爆发不来的。

好了,回到正题

这是法律经济学的最后一篇。之前的对此有的印象似乎已经有点模糊了。只记得法律经济学告诉我们许多法律上的制定可以由经济效益为出发点进行考虑,会发现许多案例有不一样的发现。

本章最明显的一个例子是火车撞小孩的故事,一个小孩在看清楚了告示之后在不会有火车经过的铁轨上玩耍,而另外5个小孩却仍然在火车要通过的铁道上玩耍。没过多久,火车匆匆而至,身为扳道工的你是否会让火车转入另一条,去撞那位对的小孩呢??

这个故事很久之前就看到过,我心目里的答案一直不该,因为一个人做错事就应该受到惩罚,凭什么让对的人替错的人去牺牲生命。法律上对就是对,错就是错,如果法律不支持对的事情,那么法律的尊严将消失的无影无踪。在文中则给出了另外几种思路,比如从小孩成长后的价值,5个富孩子家庭背景去弥补穷孩子的损失。这几种说法虽然角度独特,却也没有给予我震动感。不过当文中说道如果是100个孩子呢,1000个孩子呢,那么你是否还会去撞吗? 我感觉我的想法已经改变了,我也许会去牺牲那个人吧。法律固然是公平与正义的,可是在规则与例外之间的取舍,却显得更加慎重与微妙。(好吧,人们总能自圆其说,找到理由来暗示自己)

法律中一些规则的制定是否需要考虑情理之外的的东西。以火车撞小孩子为例,当你动摇了内心不扳轨道的决定,就说明你已经考虑了情理之外的东西,而不止是法律的公平正义了。现实中其实的确是有许多规则属于这种。对于“恋童癖”判处重刑甚至死刑就属于这个,需要加重惩罚来进行宣示警醒的作用,防患于未然,才能更好的保护孩子。而对未成年人减轻惩罚,对犯人减刑,则是因为他们未来可能会有更大更好的价值。不同的表现都是具有不同的价值的,惩罚是善后的补救措施,遏阻则是着眼于未来长远利益的展望。

哎,真的是感觉从法律经济学的角度去考虑,很多事情则都没有了法律的公平正义可言了。司法女神的尊严真的因为各种各样的情理,价值受到了极大的挑战啊。

第十三章-司法有价吗?

发表于 2017-07-06 | 分类于 《解释的工具-生活中的经济学原理》 | 阅读次数
| 字数统计 805 | 阅读时长 3

《解释的工具-生活中的经济学原理》

—–第十三章-司法有价吗?

万万没有想到距离上一篇写的文章已经过去了一个月了,看样子最近这个月的工作任务真是太大了。如果用经济学的角度去解析,之所以这么久不工作,只因为我写文章带来的收益没有我最近做项目带来的收益大。我看书加写文章所付出虽然看上去只有一小时的成本,可是这一小时从当下来看,没有为我带来任何收益,我自然会将事情的顺序排在后面。


好了,回到正题,本篇讲的是法律经济学。其实在看书的途中,我也越来越感受到文章想表达的东西,在我理解来看,其实就是成本和收益的问题。付出多少成本,拿到多少收益,怎么样实现最大成本收益比,其实就是以经济学的角度去考虑问题。文中提出来的几个事例,如违章过马路收取过路费,有些人愿意缴费用来赶时间,有人愿意0成本过马路,其实都是各自对自己时间效益的评估。政府采用收费来限制行人,而不是竖警示牌,宣传教育,也是因为觉得这样子的收益比是最高的。所以司法有价吗?缴费违章过马路对吗?过马路这件事当然不对,违法;但是我缴费也是合法维护了规则秩序,所以最终这件事只不过合乎法,违于礼而已。

文中还有一个教育部长的例子也更加证明了经济学在政府中政策的影响。教育部长站在教育领域,希望政府支持更多的精力花在特殊儿童的教育上,他拿出美国一位残疾儿童在三位医护人员的帮助下获取文学奖的例子为佐证,希望发掘更多特殊人才的潜力。可是在商言商,特殊教育固然重要,可是教育领域中的童年教育,初中教育也很重要,如果把资源集中在这一块,是不是也能发挥更多的用处呢。如果在提高一层,站在国家角度,你‘教育‘重要,那么‘环保’重要吗?‘安防’重要吗?等等重要吗?这么一对比,花在特殊教育上的资源还值得吗?教育部长的话站在自身的角度当然也没有错,合乎情理,只不过一旦吧层次提高站在更高的决策位角度,许多问题就从绝对性变为了相对性。所以教育和面包谁更重要呢?

随着这本书越读越多,其实你也能明白,真正的公平正义是没有的,对公平正义的追求,是建立在一个稳定,成熟的社会,和其背后所愿意付出的资源。司法女神希望世界充满公平,可是她也只能环抱胸前有限的空间。

第十二章-公平正义的真正意义

发表于 2017-06-08 | 分类于 《解释的工具-生活中的经济学原理》 | 阅读次数
| 字数统计 996 | 阅读时长 3

《解释的工具-生活中的经济学原理》

—–第十二章-公平正义的真正意义

第十二章实在是自己政治理解能力不够,看完都还是有点懵懂的,决定还是跨过去了。过两天抽个时间再看一遍,看看温故能不能知新。

接下去几章是法律经济学,其实还是和我们周围的生活有一些息息相关的。看完之后,仔细琢磨了生活中一些规章制度,的确隐含着很多法律经济学的影子。

就我理解来说,法律经济学的观点是衡量一件法律案件是否有理,不应该从绝对的公平正义角度着眼,而值得从经济效益的角度进行评估。

我仔细想想我们公司有些制度也有异曲同工之妙。以孕妇请假为例,假如刚开始的时候公司没有任何相关规定,那么第一位孕妇在刚开始怀孕的时候,可以带薪请假并且同时无条件享受公司各种各样的福利,那么她这种行为会不会对其他人造成心理上的伤害;她同时也在透支的后面孕妇的福利,因为如果她做的这么过分,意味着后面怀孕的人将再也无法享受到类似的福利,因为公司会从成本角度去衡量。也许让一位孕妇员工享受所有的福利应该是正义的,当时它产生的负面影响将会远远大于对公司正面的生产价值。

再以生活中一个很常见的例子进行举例。一个爱妈妈的广告也许在许多人的眼中感到的是温馨,但是一些从小没有母爱的孤儿长大之后看到这个广告,却引起了一丝被刺伤的愤怒。那么当这些人告广告商的时候,难道因为对一部分人造成了伤害,就禁止播放吗?

我相信另一部分人肯定会嗤之以鼻,心里默默念一句凭什么,我就觉得广告拍的很好。当然这只是嘴上的吐槽,我们可以更深入到第二层角度去分析。这支广告固然让一部分人愤怒,可是它同时却让更多的人知道抚慰母亲辛劳,提醒子女孺慕,甚至可能会挽回一些破碎的家庭。它产生的社会价值岂不是更大,如果你不喜欢,你可以拒绝购买广告产品,或者促使周围人一起抵制,但是不应该通过法律来限制。因为一旦以法律制度来限定,分寸的掌握就变得很微妙了。。更深程度上去看,如果一个人的喜好可以通过法律强加到另外一个人身上,将心比心,你愿意别人用同样的法律来限制你的信仰和喜好吗?你愿意让制度法律的人又那么大权力吗?

文末抛出了一个问题?既然播放伤心的广告可以,那么为什么抽烟不行呢?我的理解来看,因为抽烟对于社会生产成本产生危害将远大于其生产价值吧。我想说,如果抽烟有助于环境健康,身体长寿,同时还能帮助人们,促进社会产值。恐怕结果就完全相反了。

本章真的很有意思,文中提出了许多观点在日常生活中都显得那么无话可说。绝对的公平正义如果会将人类带向灭亡,那么他还有意义吗我们应该明白公平正义本身并不是目的,只是手段。既然是手段,也就是工具,我们当然应该选择好的工具。

123
linzx

linzx

30 日志
6 分类
9 标签
© 2018 linzx
由 Hexo 强力驱动
主题 - NexT.Pisces