第2章 let和const命令
-
let命令
-
for循环有个特别之处,循环变量是一个父作用域,循环体内部是一个单独的子作用域。
for(let i=0;i<3;i++){ let i = "abc"; console.log(i); } //输出结果 //abc //abc //abc
-
不存在变量提升
var命令会发生”变量提升”现象,即变量可以在声明之前使用,值为”undefined”。let命令改变了语法行为,它所声明的变量一定要在声明后使用,否则会报错。
console.log(foo); //undefined var foo = 2; console.log(bar); //报错 let bar;
-
暂时性死区(temporal dead zone,简称TDZ)
只要块级作用域内存在let命令,它所声明的变量就”绑定”这个区域,不再受外部的影响。
var tmp = 123; if(true){ tmp = "abc"; //报错 let tmp; //所谓"绑定" }
TDZ就意味着 typeof 操作符不在是百分之百”安全”。
有些死区比较隐蔽
function bar(x = y,y=2){ return [x,y]; }
参数x的默认值为另一个参数y,而此时y还没有声明,属于死区。
-
let 实际上为js新增了块级作用域。
function f1(){ let n = 5; if (true) { let n = 10; } console.log(n); //5 } //如果使用var定义变量n,最后输出的值是10
-
-
块级作用域
-
ES5中块级作用域中声明函数变量非法。ES6引入块级作用域,在块级作用域中,函数声明语句行为类似let,在块级作用域之外不可使用。
function f(){ console.log("I am outside!"); } (function (){ if(false){ function f() { console.log("I am inside!"); } } f(); }());
该段代码在ES5中运行会得到”I am outside!”.
但是在ES6中运行会报错。
浏览器的实现可能不遵守规范(???),而有自己的行为方式:
- 允许在块级作用域中声明函数
- 函数声明类似var,即会提升到全局作用域或函数作用域头部
- 同时,函数声明还会提升到所在块级作用域头部
故上述代码相当于:
function f(){ console.log("I am outside!"); } (function (){ var f = undefined; if(false){ function f() { console.log("I am inside!"); } } f(); }()); //Uncaught TypeError : f is not a function.
可以使用函数表示式避免该情况。
-
do 表达式
-
-
const 命令
- const 实际上保证的并不是变量的值不得改动,而是变量指向的那个内存地址不得改动。数组,对象之类使用时应注意。
- 如果真想将对象冻结,应使用Object.freeze方法。
-
ES6声明变量的6种方法
- var和function命令
- let和const命令
- import和class命令
-
顶层对象的属性
-
ES5是不区分顶层对象属性和全局变量的
window.a = 1; console.log(a); //1 a = 3; console.log(window.a); //3
-
ES6逐渐将全局变量和顶部对象隔离
为了兼容 var 和 function 命令声明的全局变量依旧是顶层对象的属性
let,const,class命令声明的全局变量不是顶层对象的属性
-
第3章 变量的解构赋值
-
只要某种数据结构具有Iterator接口,都可以采用数组形式的解构赋值。
-
ES6内部使用严格等于运算符判断一个位置是否有值
-
解构对象时匹配模式
let { foo : baz } = { foo :"aaa" } console.log(baz); //aaa let { foo } = { foo :"aaa" } console.log(foo); //aaa
let obj = { p:[ 'Hello',{ y : "World" } ] }; let { p:[x,{ y }] } = obj; console.log(x); //"Hello" console.log(y); //"World"
此时p是模式,不是变量,因此不会被赋值,如果p也要作为变量赋值,可以写成:
let obj = { p:[ 'Hello',{ y : "World" } ] }; let { p, p:[x,{ y }] } = obj; console.log(x); //"Hello" console.log(y); //"World" console.log(p); //["Hello",{y:"World"}]
另一个例子
var node = { loc : { start : { line:1, column:5 } } }; var { loc,loc:{ start },loc:{ start:{ line }}} = node; console.log(loc); //{ start: { line: 1, column: 5 } } console.log(start); //{ line: 1, column: 5 } console.log(line); //1
三次解构,分别对loc,start,line三个属性结构赋值,在最后一次解构中loc,start均是模式。
-
由于数组本质是特殊的对象,因此可以对数组进行对象属性的解构
let arr = [1,2,3]; let {0:first,[arr.length - 1] :last} = arr; console.log(first); //1 console.log(last); //3
-
字符串也可以解构赋值。此时字符串被转化成一个类似数组的对象。
let {length:len} = "hello"; console.log(len); //5
-
函数解构时默认值
注意两种写法的不同
function move({x = 0,y=0} = {}) { return [x,y]; } console.log(move({x:3,y:8})); //[3,8] console.log(move({x:3})); //[3,0] console.log(move({})); //[0,0] console.log(move()); //[0,0] function move({x,y} = {x:0,y:0}) { return [x,y]; } console.log(move({x:3,y:8})); //[3,8] console.log(move({x:3})); //[3,undefined] console.log(move({})); //[undined,undefined] console.log(move()); //[0.0]
-
用途
-
交换变量的值
let x = 1,y = 2; [x,y] = [y,x]; console.log(x,y); //2 1
-
从函数返回多个值
function example() { return [1,2,3]; } let [a,b,c] = example(); console.log(a,b,c); //1 2 3
-
函数参数的定义
//参数是一组有次序的值 function f([x,y,z]) { /*...*/ } f([1,2,3]); //参数是一组无次序的值 function f({x,y,z}) { /*...*/ } f({z:3,y:2,x:1});
-
提取json数据
解构赋值对提取json对象中的数据尤其有用
let jsonData = { id:42, status : "OK", data:[867,5309] }; let {id,status,data:number} = jsonData; console.log(id,status,number); //42 'OK' [ 867, 5309 ]
-
遍历Map结构
var map = new Map(); map.set("first","hello"); map.set("second","world"); for (let [key,value] of map){ console.log(key,value); } //first hello //second world
-
第4章 字符串的扩展
-
字符的Unicode表示法
如果直接在\u后面超过0xFFFF的数值比如\u20BB7,js会理解成\u20BB+7,由于\u20BB是一个不可打印字符,所以只会显示一个空格,后面跟一个7
ES6做出一点改进,只要将码点放入大括号中,就能正确解读该字符。即:”\u{20BB7}”
-
codePointAt可以识别32位的UTF-16字符
-
fromCodePoint 方法定义在 String 对象上,而codePointAt 方法定义在字符串的实例对象上
-
字符串遍历接口
for(let codePoint of 'foo'){ console.log(codePoint); } //f //o //o
这个遍历器可以识别大于0xFFFF的码点
-
padStart(),padEnd()
padStart最常见的用途就是拿来补全指定位数
"1".padStart(10,"0"); //"0000000001" "12345".padStart(10,"0"); //"0000012345"
第6章 数值的拓展
-
Number(“0b111”),Number(“0o10”).
-
isFinite,isNaN
这与ES5的传统方法的区别在于,传统方法先调用Number()将非数值转为数值,再进行判断,而新方法只对数值有效,对于非数值一律返回false。isNaN只对NaN才返回true,非NaN一律返回false。
-
ES6将全局方法parseInt() 和 parseFloat() 移植到了Number对象上
-
极小常量Number.EPSILON
-
安全整数–正负2的53次方
-
在V8引擎中,指数运算符与Math.pow 的实现不相同,对于特别大的运算结果,两者会有微妙的差异
-
Integer数据不能与Number类型进行混合运算
第7章 函数的扩展
-
参数变量是默认声明的,所以不用let或const再次声明
function foo(x = 1){ let x = 2; //error const x = 2;//error }
-
参数默认值不是传值的,而是每次都重新计算默认值表达式的值。也就是说,参数默认值是惰性求值。
let x = 99; function foo(p = x + 1) { console.log(p); } foo(); //100 x = 100; foo(); //101
上面的代码中,参数p的默认值是x+1。这时,每次调用函数foo都会重新计算x+1,而不是默认p等于100
-
函数的length属性
- 设置了默认值之后,函数的length属性将返回没有指定默认值的参数个数。
- 设置了默认值的参数不是尾参数,那么length属性也不再计入后面的参数。
-
作用域
一旦设置了参数的默认值,函数在进行声明初始化的时候,参数会形成一个单独的作用域。
let x = 1; function f(y = x) { let x = 2; console.log(y); } f(); //1
-
应用
可以利用参数默认值指定某一个参数不得省略,如果省略就抛出一个错误
function throwMissing() { throw new Error("Missing parameter"); } function foo(mustBeProvided = throwMissing()) { return mustBeProvided; } foo();
-
rest参数
- 形式:…变量名,类似java的那个啥可变参数吧
- rest参数中的变量代表一个数组,所以数组特有的方法都可以用于这个变量
- rest参数之后不能再有其他参数
-
严格模式
-
函数内部不可以使用严格模式
-
规避限制方法
-
使用全局严格模式
-
把函数包在一个无参数的立即执行函数里面
const doSomething = (function () { "use strict"; return function (value = 42) { return value; }; }());
-
-
-
name属性
注意在赋值匿名函数时ES5和ES6的差异
ES5返回空字符串,ES6返回实际函数名
Function 构造函数返回的函数实例,name属性值为anonymous
bind返回的函数,name属性值会加上 bound 前缀
-
箭头函数
-
箭头函数的一个作用是简化回调函数
-
箭头函数体的this对象是定义时所在的对象,而不是使用时所在的对象
function foo() { setTimeout(()=>{ console.log(this.id); },100); } var id =21; foo.call( {id:42} ); //42
-
箭头函数体没有自身的this,所以可以达到”绑定”this的作用
-
-
箭头函数实现管道调用(QUQ没能看懂….mark一下以后看
const pipleline = (...funcs) => val => funcs.reduce((a,b) => b(a),val);
const plus1 = a=>a+1;
const mult2 = a=>a*2;
const addThenMult = pipleline(plus1,mult2);
console.log(addThenMult(5)); //12
-
尾调用的优化
-
函数调用会形成一个”调用记录”,即调用帧,保存调用位置和内部变量等信息。如果A函数的内容调用函数B,那么在A的调用帧上方会形成一个B的调用帧,等到B的运行结束,将结果返回到A,B的调用帧才会消失。如果函数B内部还调用了C就会还有一个C的调用帧。所有的调用帧就会形成一个调用栈。
-
尾调用由于是函数的最后一步操作,所以,不需要保存外层函数的调用帧,因为调用位置,内部变量等信息都不会再用到,直接用内层函数的调用帧取代外层函数的即可。
function f(){ let m = 1; let n = 2; return g(m+n); } //等同于 function f(){ return g(3); } f(); //等同于 g();
这就是”尾调用优化”,即只保留内层函数的调用帧。如果所有函数都是尾调用,那么完全可以做到每次执行时调用帧只有一项,这将大大节省内存。
PS:只有不在用到外层函数的内部变量的时候。内层函数的调用帧才会取代外层函数的调用帧,否则无法进行尾调用优化。
-
尾递归
-
敲黑板划重点(嘻嘻
-
一旦使用递归,尽量使用尾递归
-
ES6的尾递归模式只在严格模式下才开启,正常模式是无效的
这是因为,在正常模式下函数内部有两个变量,可以跟踪函数的调用栈。
- func.arguments:返回调用函数的参数
- func.caller:返回调用当前函数的那个函数
尾调用优化发生时,函数的调用栈会被改写,因此上面两个变量会失真。严格模式禁用这两个变量,所以尾调用模式仅在严格模式下生效。
-
避免递归——蹦床函数
蹦床函数可以将递归执行转化为循环执行
function trampoline(f){ while(f && f instanceof Function){ f = f(); } return f; }
这就是蹦床函数的一个实现,可以通过蹦床函数将递归执行转化为循环执行
function sum(x,y){ if(y > 0) return sum(x+1,y-1); else return x; } //转化为蹦床参数函数 function sum(x,y){ if(y>0) return sum.bind(null,x+1,y-1); else return x; } console.log(trampoline(sum(1,10000))); //10001
-
-
第8章 数组的拓展
-
拓展运算符
-
… 如同rest参数的逆运算
console.log(...[1,2,3]); //1 2 3
-
该运算符只要用于函数调用
function push(array, ...intems) { array.push(...intems); } let ar = []; push(ar,1,2,3); console.log(ar); //[1,2,3]
-
替代数组的apply方法
由于拓展运算符可以展开数组,所以不再需要使用apply方法将数组转为函数参数
function f(x,y,z) { console.log(x,y,z); } var args = [0,1,2]; //ES5 f.apply(null,args); //ES6 f(...args); //ES5 Math.max.apply(null,[14,3,77]); //ES6 Math.max(...[14,3,77]); //等同于 Math.max(14,3,77); var arr1 = [0,1,2]; var arr2 = [3,4,5]; //ES5 Array.prototype.push.apply(arr1,arr2); //ES6 arr1.push(...arr2);
-
应用
-
合并数组
//ES5 [1,2].concat(more); //ES6 [1,2,...more]; var arr1 = [1,2]; var arr2 = [3,4]; var arr3 = [5,6]; //ES5 arr1.concat(arr2,arr3); //[ 1, 2, 3, 4, 5, 6 ] //ES [...arr1,...arr2,...arr3]; //[ 1, 2, 3, 4, 5, 6 ]
-
与解构赋值结合
const [first,...rest] = [1,2,3,4,5]; //只能放在最后 first //1 rest //[2,3,4,5]
-
函数返回值
-
将字符串转为数组
尤其是字符串中还有编码大于0xFFFF的特殊字符
-
实现了Iterator接口的对象
对于那些没有Iterator的对象拓展运算符会报错
可以用Array.from方法转成数组
-
Map和Set结构,Generator函数
-
-
-
Array.from方法
-
Array.from方法用于将两类对象转成真正的数组:类似数组的对象和可遍历的对象
-
所谓类似对象本质:必须有length属性
let arraylike = { '0' : 'a', '1':'b', // 'asd':'1', length:3 } console.log(Array.from(arraylike)); //[ 'a', 'b', undefined ]
-
Array.from 可以将各种值转化为真正的数组。这实际上以为这,只要有一个原始数据结构,就可以先对它的值进行处理,然后转成规范的数组结构,进而可以使用数量众多的数组方法。
Array.from({length : 2},()=>'java'); //'jack','jack'
-
-
Array.of方法
将一组值转化为数组,弥补构造函数Array的不足
Array(); //[] Array(3); //[,,] Array(2,3,4)//[2,3,4]
-
copyWithin
数组实例的copyWithin方法会在当前函数的数组内部将指定的成员复制到其他位置,然后返回当前数组。也就是说使用这个方法会修改当前数组。
它接受3个参数
- target 从该位置开始替换数据
- start 从该位置开始读数据,默认从0
- end 到该位置前停止读数据,默认数组长度
var a = [1,2,3,4,5].copyWithin(0,3); console.log(a); //[4,5,3,4,5]
-
find和findIndex
参数为一个回掉函数,分别找到一个满足该函数的成员,和索引
var a = [1,3,-5,10].find(i=>i<0); console.log(a); //-5 var a = [1,3,-5,10].findIndex(i=>i<0); console.log(a); //2
-
fill
定值填充数组,3个参数,填充值,填充起点,填充终点
-
entries,keys和values遍历数组,分别获取键值对,建名以及键值
-
includes
判断是否包含某个值
indexOf其内部使用严格等于进行判断会导致对NaN的误判
includes不会
-
数组的空位
-
[ , , ,]
-
空位不是undefined,一个位置的值等于undefined依然是有值的。空位是没有任何值的。
-
ES5
- forEach,filter,every和some都会跳过空位
- map会跳过空位,但是保留这个值
- join和toString会将空位视为undefined,而undefined和null会被处理成空字符串
ES6
- 明确将空位视为undefined
- copyWithin直接将空位复制
- fill会将空位视为正常位置
- for…of也会遍历空位
-
第9章 对象的扩展
-
Object.is()
与严格相等运算符的行为基本一致
不同的只有两个
- +0不等于-0
- NaN等于自身
-
Object.assign() 复制源对象属性
-
第一个参数是目标对象,后面的参数都是源对象
-
多个源对象有同名属性发生覆盖
-
如果只有一个参数,直接返回该参数
-
只复制源对象自身的属性,不复制继承属性,也不复制不可枚举的属性
-
Symbol值也会被复制
-
只进行浅复制,即复制引用
-
可以用来处理数组,把数组视为0,1,2的对象
console.log(Object.assign([0,1,2],[3,4])); //[ 3, 4, 2 ]
-
常见用途
- 为对象添加属性
- 为对象添加方法
- 克隆对象
- 合并多个对象
- 为属性指定默认值
-
-
属性的可枚举
ES5有3个操作会忽略enumerable为false的属性
- for…in 循环
- Object.keys()
- JSON.stringify()
ES6新增一个Object.assign()
上述4个操作中,只有for…in会返回继承的属性。实际上,引入enumerable的最初目的就是让某些属性可以规避掉for…in操作。比如,对象原型的toString方法以及数组的length属性就是通过这样的手段而不会被for…in遍历到。
-
属性的枚举
ES6总共有5种方法,可以遍历对象的属性
-
for…in
可以遍历对象自身的和继承的可枚举属性,不含symbol属性
-
Object.keys(obj)
返回一个数组,包括对象自身的(不含继承的)所有可枚举属性(不含symbol属性)
-
Object.getOwnPropertyNames(obj)
返回一个数组,包括对象自身的所有属性(不含symbol属性,但是包含不可枚举属性)
-
Object.getOwnPropertySymbols(obj)
返回一个数组,包括对象自身的所有symbol值
-
Reflect.ownKeys(obj)
返回一个数组,包含对象自身的所有属性,不管是属性名还是Symbol还是字符串,也不管是否可枚举
以上所有属性都遵守同样的属性遍历次序规则
- 首先遍历所有属性名为数值的属性,按照数字排序
- 其次遍历所有属性名为字符串的属性,按照生成时间排序
- 最后遍历所有属性名为Symbol值的属性,按照生成时间排序
-
-
对象的拓展元素符
-
结构赋值
let {x,y,...z} = {x:1,y:2,a:3,b:4}; console.log(z); //{a:3,b:4}
解构赋值的复制是浅复制,即如果一个键的值是复合类型的值,那么为复制引用
-
拓展运算符可以用于合并两个对象
let ab = {...a,...b}
-
-
Object.getOwnPropertyDescriptors
该方法的引入主要是为了解决Object.assign()无法正确复制get属性和set属性的问题
-
Null传导运算符
?. 只要其中一个返回null或undefined,就不要在计算,而是返回undefined
第10章 Symbol
-
Symbol,表示独一无二的值
var s1 = Symbol("foo"); var s2 = Symbol("foo"); s1 === s2 //false
上面s1和s2都是Symbol函数的返回值,并且参数相同,但是他们是不相等的。
-
Symbol值不能与其他类型的值进行运算
-
作为属性名的Symbol
var mySymbol = Symbol(); //第一种 var a = {}; a[mySymbol] = "hello"; //第二种 var a = { [mySymbol]:"hello" } //第三种 var a = {}; Object.defineProperties(a,mySymbol,{value:"hello"});
-
属性名的遍历
Symbol作为属性名,该属性不会出现在for…in,for…of 循环中,也不会被Object.keys(),Object.getOwnPropertyNames()返回
Symbol值作为名称的属性不会被常规方法遍历的到,可以利用这个特性为对象定义一些非私有但是只用于内部的方法
-
Symbol.for(),Symbol.keyFor()
var s1 = Symbol.for("foo"); var s2 = Symbol.for("foo"); console.log(s1 === s2); //true
Symbol.for与Symbol都会生成新的Symbol,但是前者会在被登记的全局环境中搜索,后者不会。
-
内置的Symbol值
-
Symbol.hasInstance
Symbol.hasInstance属性是指向内部的方法,对象使用instanceof运算符时会调用这个方法,判断该对象是否为某个构造函数的实例,比如 foo instanceof Foo 在语言内部调用的就是Foo[Symbol.hasInstance](foo)
class MyClass { [Symbol.hasInstance](foo){ return foo instanceof Array; } } console.log([1,2,3] instanceof new MyClass()); //true
-
Symbol.isConcatSpreadable
表示对象使用,Array.prototype.concat时是否可以展开
数组默认行为是可以展开的,对象是不展开的
-
….还有9个以后查文档吧XDDD
-
第11章 Set和Map数据结构
-
Set类似数组,但是成员的值是唯一的,没有重复
const items = new Set([1,2,3,4,5,5,5,5]); console.log(items.size); //5
所以可以用set数组去重
[...new Set([1,2,3,4,5,5,5,5])] //[ 1, 2, 3, 4, 5 ]
-
向set加入值不会发生类型转化,5和”5”是两个不同的值。set内部判断两个值是否相等使用的算法类似与严格等于,主要区别是set中NaN等于自身,严格等于不等于
另外两个对象总是不相等
let a = {}; let set = new Set(); set.add(a); set.add(a); console.log(set.size);//1
-
Array.from方法可以将Set转成数组(解构也行啊!)
-
Set实例的属性与方法
-
操作方法
- add
- delete
- has
- clear 清除所有成员
-
遍历操作
- keys 返回键名的遍历器
- values 返回键值的遍历器
- entries 返回键值对的遍历器
- forEach 使用回调函数遍历每个成员
let set = new Set(["red","green","blue"]); for(let item of set.keys()) console.log(item); for(let item of set.values()) console.log(item); for (let item of set.entries()) console.log(item); // red // green // blue // red // green // blue // [ 'red', 'red' ] // [ 'green', 'green' ] // [ 'blue', 'blue' ]
let set = new Set(["red","green","blue"]); set.forEach((key,value)=>{ console.log(key,value); }); // red red // green green // blue blue
-
遍历的应用
let a = new Set([1,2,3]); let b = new Set([2,3,4]); //并集 let union = [...new Set([...a,...b])]; console.log(union); //[ 1, 2, 3, 4 ] //交集 let interset = [...new Set([...b].filter(x=>a.has(x)))]; console.log(interset); //[ 2, 3 ] //差集 let difference = [...new Set([...b].filter(x=>!a.has(x))),...new Set([...a].filter(x=>!b.has(x)))]; console.log(difference);//[ 4, 1 ]
在遍历操作中同步改变原来的set结构,目前没有直接方法,但是可以变通XDDD
let a = new Set([1,2,3]); a = new Set([...a].map(x=>x*2)); //Set { 2, 4, 6 }
-
-
WeakSet
和Set区别
-
WeakSet的成员只能是对象,而不能是其他类型的值
-
WeakSet的对象都是弱引用,即垃圾回收机制不考虑WeakSet对该对象的引用
const a = [[1,2],[3,4]]; const ws = new WeakSet(a);//WeakSet {[1,2],[3,4]} //成为WeakSet的成员的是a数组的成员,而不是a数组本身
-
WeakSet没有size属性,没有办法遍历其成员
-
-
Map——键值对的集合
键的范围不限于字符串,各种类型的值都可以当做键
基本用法:
const m = new Map(); const o = {p:"hello world"}; m.set(o,"xixi"); console.log(m.get(o)); //xixi //只有对同一个对象的引用,Map才会将其视为同一个键
-
Map遍历方法同Set,也同样适用…解构
const m = new Map([ [1,"one"], [2,"two"], [3,"three"] ]); console.log([...m]); //[ [ 1, 'one' ], [ 2, 'two' ], [ 3, 'three' ] ]
结合数组的map,filter方法可以实现map的遍历和过滤
let m = new Map([ [1,"one"], [2,"two"], [3,"three"] ]); [...m].map(([key,value])=>console.log(key,value)); m = new Map([...m].filter(([key,value])=>key<3));
-
WeakMap类似WeakSet
WeakMap弱引用的只是键名,键值依然是正常引用
-
可以把DOM结点和其绑定事件放在WeakMap里面,一旦这个结点删除该状态就会自动消失,不存在泄漏危险。
第12章 Proxy
-
语法
// proxy = new Proxy(target,handler); let obj = new Proxy({},{ get:function (target,key,receiver) { console.log("getting..."); return Reflect.get(target,key,receiver) }, set:function (target,key,value,receiver) { console.log("setting..."); return Reflect.get(target,key,value,receiver); } }); obj.a = 3; obj.a++; //setting... //getting... //setting...
-
要使proxy起作用,必须针对proxy实例操作,而不是对目标对象操作
-
一个技巧是将proxy对象设置到object.proxy属性上,从而可以在object对象上调用
let obj = { proxy:new Proxy(this,{ get:function (target,property) { return 35; } }), a:20 }; console.log(obj.a); //20 console.log(obj.proxy.a); //35
-
proxy对象也可以作为其他对象的原型实例
let proxy = new Proxy({},{ get:function (target,property) { return 35; }, }); let obj = Object.create(proxy); console.log(obj.time); //35 //obj对象本身没有time属性,所以根据原型链会在proxy对象上读取该属性,导致拦截
-
拦截操作
- get(target,key,receiver)
- set(target,key,value,receiver)
- has(target,key)
- deleteProperty(target,key)
- ownKeys(target,key)
- getOwnPropertyDescriptor(target,key)
- defineProperty(target,key,propDesc)
- ……
-
如果一个属性不可配置或者不可以写,则该属性不能被代理,通过proxy对象访问该属性会出错
-
可以在set属性拦截,即每当对象发生改变是自动更新DOM
-
有时我们会在对象上设置内部的属性,属性名的第一个字符使用下划线开头,表示这些属性不你应该被外界使用,结合proxy的set和get拦截就可以防止这些内部属性被外部读/写
-
Proxy实例作为函数调用时,就会被apply方法拦截
let twice = { apply(target,ctx,args){ return Reflect.apply(...arguments)*2; } }; function sum(left,right) { return left+right; } let proxy = new Proxy(sum,twice); console.log(proxy(1,2)); //3 console.log(proxy.apply(null,[1,2])); //3 console.log(proxy.call(null,1,2)); //3
-
has方法可以用来拦截HasProperty操作,即判断对象是否具有某个属性时,这个方法会生效。典型操作是in运算符。PS:has 对for…in 循环不生效
let handler = { has(target,key){ if(key[0] === "_"){ return false; } return key in target; } }; let target = { _prop : "foo",prop:"foo" }; let proxy = new Proxy(target,handler); console.log("_prop" in proxy); //false console.log("prop" in proxy); //true for (let key in proxy) console.log(key); //_prop //prop
-
has方法拦截的是HasProperty操作,而不是HasOwnProperty操作,即has方法不判断一个属性是对象自身的属性还是继承的属性。
-
ownKeys方法用来拦截对象自身属性的读取操作
- Object.getOwnPropertyNames
- Object.getOwnPropertySymbols
- Object.keys,有三类属性会被自动过滤
- 目标对象不存在的属性
- 属性名为Symbol值
- 不可遍历属性
如果目标对象自身包含不可配置的属性,则该属性必须被ownKeys方法返回,否则会报错
如果目标对象是不可拓展的,这时ownKeys方法返回的数组之中必须包含原对象的所有属性,且不能包含多余的属性,否则会报错
-
Proxy.revocable的一个使用场景是,目标对象不允许直接访问,必须通过代理返问,一旦访问结束。就收回代理权不再返回。
let target = {}; let handler = {}; let {proxy,revoke} = Proxy.revocable(target,handler); proxy.foo = 123; console.log(proxy.foo); //123 revoke(); //取消proxy实例 console.log(proxy.foo); //TypeRrror
-
虽然Proxy可以代理针对目标对象的访问,但它不是目标对象的透明代理,即不做任何拦截的情况下也无法保证与目标对象行为一致。主要原因就是在Proxy代理的情况下,目标对象内部的this关键字会指向Proxy代理。
const target = { m:function () { console.log(this === proxy); } }; const handler = {}; const proxy = new Proxy(target,handler); target.m(); //false proxy.m(); //true
-
此外,有些原生对象的内部属性只有通过正确的this才能获取,所以Proxy也无法代理这些原生对象的属性。
const target = new Date(); const handler = {}; const proxy = new Proxy(target,handler); proxy.getDate(); //TypeError: this is not a Date object. //这时this绑定原始对象就可以解决这个问题 const target = new Date(); const handler = { get(target,prop){ if(prop=="getDate"){ return target.getDate.bind(target); } return Reflect.get(target,prop); } }; const proxy = new Proxy(target,handler); console.log(proxy.getDate()); //19
-
Proxy对象可以拦截目标对象的任何属性,这使它可以很合适用来编写web服务的客户端
function creatWebService(baseUrl) { return new Proxy({},{ //拦截操作... }); }
第13章 Reflect
-
reflect将object对象的一些明显属于语言内部的方法放到reflect对象上,并修改某些object方法的返回结果
-
reflect对象的方法与proxy对象的方法一一对应,只要是proxy对象的方法,就能在reflect对象上找到对应的方法,这就使proxy对象可以方便地调用对应的reflect方法来完成默认行为,作为修改行为的基础。
let proxy = new Proxy({},{ get: function(target,key){ console.log("get",target,key); return Reflect.get(target,key); } }); proxy.a = 1; console.log(proxy.a); //get { a: 1 } a //1
-
如果name属性部署了读取函数,则读取函数的this绑定receiver
//get let obj = { foo:1, bar:2, get baz(){ return this.foo + this.bar; }, }; let a = { foo:4, bar:4 }; console.log(Reflect.get(obj,"baz",a));//8 console.log(Reflect.get(obj,"baz"));//3
//set let obj = { foo:1, bar:2, set baz(value){ return this.foo = value; }, }; let a = { foo:4, bar:4 }; Reflect.set(obj,"baz",10,a); console.log(obj.foo); //1 console.log(a.foo); //10
-
Reflect.set会触发Proxy.defineProperty拦截
-
reflect.ownKeys方法返回对象的所有属性,基本等同于Object.getOwnPropertyNames与Object.getOwnSymbols之和
-
用proxy实现观测者模式
第14章 Promise对象
-
Promise是异步编程的一种解决方案,简单来说就是一个容器,里面保存着某个为了才会结束的事件的结果。从语法上来说,Promise是一个对象,从它可以获取异步操作的消息。Promise提供统一的API,各种异步操作都可以用同样的方法进行处理。
Promise对象有以下两个特点:
- 对象的状态不受外界的影响。Promise对象代表一个异步操作,有三种状态:Pending(进行中),Fulfilled(已成功),Rejected(已失败)。只有异步操作的结果可以决定当前是哪种状态。
- 一旦状态改变就不会再变,任何时候都可以得到这个结果。
-
基本用法
let promise = new Promise(function (resolve,reject) { // do something... console.log("sdfa"); if (/*异步操作成功*/) { resolve(args); } else{ reject(args); } });
-
一旦创建立即执行
-
状态一旦改变立即执行then函数
-
如果调用resolve函数和reject函数时带有参数,那么这些参数会传递给回调函数。reject函数的参数通常是Error对象的实例,表示抛出错误,resolve函数的参数还可以是promise对象,此时要注意
-
resolve和reject并不会中介promise参数函数的执行
-
then方法返回的是一个新的promise实例
其return的值为返回promise实例then方法的参数
建议使用catch方法
-
Promise.race方法同样是将多个promise包装成一个实例,其中一个promise改变状态,该实例状态就发生改变
-
Promise.resolve参数为thenable对象,个人理解:
Promise将这个对象转为promise对象,然后立即执行thenable对象的then方法,其中then将变成参数为(resolve,reject)的方法,该then方法函数行为和promise构造函数行为相似
let thenable = { then:function (resolve,reject) { // resolve(42); console.log("thenable"); // resolve(1); reject(1); } }; let p1 = Promise.resolve(thenable); p1.then(value => console.log(value) ).catch(value => console.log(value));
第15章 Iterator和for…of循环
-
Iterator接口的目的是为所有数据结构提供统一的访问机制,即for…of循环
-
数据结构只要部署了Iterator接口,我们就称这种数据结构为可遍历的
-
默认的Iterator接口部署在数据结构的Symbol.iterator属性
-
遍历器对象必须部署next方法,还可以具有return方法和throw方法
-
return方法
如果for…of心有讯黄提前退出,就会调用return方法;如果一个对象在对象完成遍历前需要清理或释放资源,就可以部署return方法
return方法必须返回一个对象
-
throw方法
-
-
for…of循环调用遍历器接口,数组的遍历器接口只返回具有数字索引的属性
let arr = [1,2,3]; arr.foo = "hhh"; for (let i in arr) console.log(i); //1,2,3,foo for (let i of arr) console.log(i); //1,2,3
-
set和map遍历的顺序是按照各个成员被添加进数组的顺序,set返回一个值,map返回一个[键,值]的数组
-
for…in循环的缺点
- 数组的键名是数字,但是for…in是以字符串即”0”,”1”,”2”
- for…in还会遍历手动添加的键名,甚至原型链上的键
- for…in有时会以任意顺序遍历
- 不可以配合break,continue,return
食用
第16章 Generator函数的语法
-
执行Generator函数会返回一个遍历器对象
-
遍历器对象的next方法运行逻辑如下
- 遇到yield语句就会暂停执行后面的操作,yield后面表达式的值作为返回的对象的value的属性值
- 下次调用next方法的时候继续执行,直到下一条yield语句
- 如果没有遇见yield语句,就一直运行到函数结束,直到return语句,return后面表达式的值作为返回的对象的value值
- 没有return,value为”undefined”
function* f() { yield 123; yield 456; return 890; } h = f(); console.log(h.next());//{ value: 123, done: false } console.log(h.next());//{ value: 456, done: false } console.log(h.next());//{ value: 890, done: true } console.log(h.next());//{ value: undefined, done: true }
PS:只有调用时才会进行yield语句后面的表达式
//!!输出顺序....!! function *f() { console.log("hello",(yield )); console.log("hell0" , (yield 123)); } let h = f(); console.log(h.next()); //{ value: undefined, done: false } //hello undefined console.log(h.next()); //{ value: 123, done: false } //hell0 undefined console.log(h.next()); //{ value: undefined, done: true } console.log(h.next()); //{ value: undefined, done: true }
-
next方法的参数
yield语句本身没有返回值,或者说总返回undefined。
next方法可以带一个参数,作为上一条yield语句的返回值
function* f() { for (let i = 0;true;i++){ let reset = yield i; if(reset) i = -1; } } let g = f(); console.log(g.next()); //{ value: 0, done: false } console.log(g.next()); //{ value: 1, done: false } console.log(g.next(undefined)); //{ value: 2, done: false } console.log(g.next(true)); //{ value: 0, done: false } //通过给next方法添加参数使generator函数重启
即,可以通过next方法的参数就有办法在generator函数运行的不同阶段从外部向内部注入不同的值,从而调整函数的行为
-
generator函数内部署的try…catch可以被throw方法捕获
-
throw方法执行的时候自带执行一次next
-
for…of循环
function* objectEntries() { let proKeys = Object.keys(this); for (let prokey of proKeys){ yield [prokey,this[prokey]]; } } let jane = { first:"Jane", last:"Doe" }; jane[Symbol.iterator] = objectEntries; for (let [key,value] of jane) console.log(`${key}:${value}`); //first:Jane //last:Doe
-
yield* 语句
yield* 命令可以很方便地取出嵌套数组的所有成员
const tree = ['a',['b','c'],['d','e']]; function* iterTree(tree) { if(Array.isArray(tree)){ for(let i = 0 ; i <tree.length;i++) yield *iterTree(tree[i]); } else yield tree; } console.log([...iterTree(tree)]);//[ 'a', 'b', 'c', 'd', 'e' ]
-
generator函数this
function *f() { this.a = 11; } let obj = f(); console.log(obj.a);//undefined
让generator函数返回一个正常的对象实例,既可以用next方法,也可以获取正常的this
变通方法:生成一个空的对象,使用call绑定this,这样这个空对象就是generator函数的实例对象
function* F() { this.a = 1; yield this.b = 2; yield this.c = 3; } let obj = {}; let f = F.call(obj); console.log(f.next()); //{ value: 2, done: false } console.log(f.next()); //{ value: 3, done: false } console.log(f.next()); //{ value: undefined, done: true } console.log(obj.a); // 1 console.log(obj.b); // 2 console.log(obj.c); // 3
还可以继续往下统一
function* F() { this.a = 1; yield this.b = 2; yield this.c = 3; } let f = F.call(F.prototype); //这样就把属性绑定到f上了,不过注意原型链 console.log(f.next()); //{ value: 2, done: false } console.log(f.next()); //{ value: 3, done: false } console.log(f.next()); //{ value: undefined, done: true } console.log(f.a); // 1 console.log(f.b); // 2 console.log(f.c); // 3
-
generator函数是实现状态机的最佳结构
function *clock() { while (true){ console.log("Tick!"); yield ; console.log("Tock!"); yield ; } }
-
应用
-
异步操作的同步化表达
处理异步操作,改写回调函数
function* loadUI() { showLoadingScreen(); yield loadUIDataAsynchronously(); hideLoadingScreen(); } let loarder = loadUI(); //加载UI loarder.next(); //卸载UI loarder.next();
-
控制流管理
step1(function (value1) { step2(function (value2) { step3(function (value3) { step4(function (value4) { //do something... }); }); }); }); //改善 let steps = [step,step2,step3,step4]; function* iterateSteps(values) { for (var i = 0 ; i<steps.length ; i++){ let step = steps[i]; yield step(); } }
-
部署Iterator接口
-
作为数据结构
第17章 Generator函数的异步应用
-
Generator函数执行的一个真实的异步任务
var fetch = require("node-fetch"); function* gen() { var url = "https://api.github.com/users/github"; var result = yield fetch(url); console.log(result.bio); } var g = gen(); var result = g.next(); result.value.then(function (data) { //fetch模块返回的是一个Promise对象 return data.json(); }).then(function (data) { g.next(data); });
-
编译器的”传名调用”的实现往往是将参数放到一个临时的函数之中,再将这个临时函数传入函数体。这个函数就是Thunk函数。
-
Thunk函数
//正常版本的readFile fs.readFile(fileName,callback); //Thunk版本的readFile let Thunk = function (fileName){ return function (callback){ return fs.readFile(fileName, callback); } } let readFileThunk = Thunk(fileName); readFileThunk(callback);
-
Thunkify模块
-
实现和上面差不多吧= =
-
看个sample吧。。。
function f(a, b, callback){ var sum = a + b; callback(sum); } var ft = thunkfify(f); var print = console.log.bind(console); ft(1,3)(print); // 4
-
-
Thunk函数的自动流程管理(不是很理解orz…
function run(fn) { let gen = fu(); function next(err,data) { let result = gen.next(data); if(result.done) return; result.value(next); } next(); } function* g() { // ... } run(g);
-
co模块
function* gen(){ // ... } //使用co模块无须编写generator函数的执行器 var co = require("co"); co(gen); //放入co容器就可以自动执行,co函数返回一个promise对象 //yield命令后面只能是Thunk函数或者promise对象 v4.0好像只能支持promise了...
第18章 async函数
-
async函数
- 内置执行器
- 更好的语义
- 更广的实用性
- 返回promise
-
用法
async返回一个promise对象,可以使用then方法添加回调函数。当函数执行的时候,一旦遇到await就会先返回,等到异步操作完成,再接着执行函数体内后面的语句。
async函数返回一个promise对象
-
await命令后面跟着的是一个promise对象,如果不是那么会隐式转换为promise对象,只要一个async函数中的一个await语句后面的promise变成reject,那么整个async函数都会中断执行
所以可以在await后面的promise对象加一个catch方法
-
await命令后面的promise对象的运行结果可能是rejected,所以最好把await命令放在try…catch中
-
多个await命令后面的异步操作如果不存在继发关系的话,最好让它们同时触发。
let [foo,bar] = await Promise.all([getFoo(),getBar()]);
-
map并发执行
async function logInOrder(urls){ //并发读取远程url const textPromise = urls.map(async url=>{ const response = await fetch(url); return response.text; }); //按次序输出 for(const textPromises of textPromsises){ console.log(await textPromise); } }
第19章 Class的基本语法
-
ES6的类完全可以看作构造函数的另一种写法
class Point { } console.log(typeof Point) //function console.log(Point === Point.prototype.constructor) //true
-
构造函数的prototype属性在ES6的”类”上继续存在。事实上,类的所有方法都定义在类的prototype属性上。
在类的实例上调用方法,就是调用原型的方法
-
类内部定义的方法是不可以枚举
-
类和模块的内部默认使用严格模式
-
使用new调用构造函数会自动创建一个新的对象,调用构造函数的一个重要特征是,构造函数的prototype属性被新对象作为原型。这意味着通过同一个构造函数创建的所有对象都继承一个相同的对象
-
constrictor方法,构造函数
-
实例的属性除非显示的定义在其本身(即this对象)上,否则都是定义在原型上 -
不存在变量提升
-
类的方法内部如果含有this,它将默认指向类的实例。
class Logger { printName(name = "there"){ this.print(`hello,${name}`); } print(text){ console.log(text); } } const logger = new Logger(); const {printName} = logger; printName();//TypeError: Cannot read property 'print' of undefined //this默认指向Logger类的实例,但是如果将这个方法解构出来单独使用,此时this指向当前运行环境,因为找不人print方法导致报错
解决方法:
//可以在构造方法中绑定this constructor(){ this.printName = this.printName.bind(this); }
-
class的静态方法
如果在一个方法前加上static关键字,就表示该方法不会被实例继承而是通过类直接调用
静态属性支持不了好像
-
class内部调用new.target返回当前class
子类继承父类的时候,new.target会返回子类
第20章 Class的继承
-
class可以通过extends关键字实现继承
-
子类必须在constructor方法中调用super方法,否则新建实例会报错。这是因为子类没有自己的this对象,而是继承父类的this对象,然后对其加工,不调用super方法,子类就没有没法拿到this对象
ES5的继承实质是先创建子类的实例对象this,然后再将父类的方法添加到this上面,ES6的继承机制完全不同,实质是先创造父类的实例对象this,然后再用子类的构造函数修改this
-
在子类构造函数中,只有调用super之后才可以使用this关键字
-
super可以当做方法,也可以当做对象使用,只有在constructor中才可以当方法使用,并且super作为对象只能拿到父类原型上的方法
-
类的prototype和__proto__
class拥有prototype和__proto__两个属性,因此存在两条继承链
- 子类的__proto__属性表示构造函数的继承,总是指向父类
- 子类的prototype属性的__proto__属性表示方法的继承,总是指向父类的prototype
class A { } class B extends A{ } console.log(B.__proto__ === A); //true console.log(B.prototype.__proto__ === A.prototype); //true
可以这样理解:作为一个对象,子类B的原型(__proto__属性)是父类A;作为一个构造函数,子类B的原型(prototype)是父类的实例
-
expends的继承目标
-
子类继承Object类
这种情况下,A其实就是构造函数Object的复制,A的实例就是Object的实例
-
不存在任何继承
这种情况下直接继承Function.prototype
-
继承null
-
-
class可以继承原生构造函数
-
Mixin模式的实现
function mix(...mixins) { class Mix{} for(let mix in mixins){ copyProperties(Mix,mix); copyProperties(Mix.prototype,mix.prototype); } return Mix; } function copyProperties(target,source) { for(let key of Reflect.ownKeys(source)){ if(key !== "constructor" && key !== "prototype" && key !== "name"){ let desc = Object.getOwnPropertyDescriptor(source,key); Object.defineProperties(target,key,desc); } } } //调用 class A extends mix(B,C){ //... }
第21章 修饰器
=。=目前ES6不支持,babel支持所以嘻嘻摸了
第22章 Module的语法
-
CommonJS模块就是对象,输入时必须查找对象属性
let { stat, exists,readFile } = require("fs"); //等同于 let _fs = require("fs"); let stat = _fs.stat; let exists = _fs.exists; let readFile = _fs.readFile;
即整体加载fs模块,生成一个对象。然后再从这个对象读取这3个方法。即”运行时加载”
ES6的模块不是对象,而是通过export命令显示指定输出的代码,再通过import命令输入
//ES6模块加载 import { stat, exists,readFile } from "fs";
即从fs模块中加载了3个方法,而不加载其他方法,即”编译时加载”或”静态加载”
-
模块功能主要是由两个命令构成:export和import
export
// 变量 // export let firstName = "firstName"; // export let secondeName = "secondeName"; // export let year = 1988; // 函数 // export function sayName(argument) { // console.log(firstName); // } // 统一输出 // let a = "1"; // let b = "2"; // let c = "3"; // // export {a,b,c}; // 变名输出 // export { a as aa,b as bb,c as cc }; // 默认输出 // export default function foo(){ // console.log("test"); // } // 继承输出 // export * from "./anothermodule"; // export let d = "4";
import
//普通变量 // import { firstName,secondeName,year } from "./module"; // console.log(firstName); // 函数 // import { sayName } from "./module"; // sayName(); // // import { a,b,c } from "./module"; // import {aa as a, bb as b,cc as c } from "./module"; // console.log(a,b,c); // // import * as all from "./module"; // console.log(all.aa); // import xixi from "./module"; // xixi(); // import * as all from "./module"; // console.log(all.a); //
第23章 Module的加载实现
-
ES6模块与CommonJS模块的差异
- CommonJS模块输出的是一个值的复制,ES6模块输出的是值的引用
- CommonJS模块是运行时加载,ES6模块是编译时输出接口
第二个差异是因为CommonJS加载的是一个对象(即module.exports属性),该对象只有在脚本运行结束时才会生成。而ES6模块不是对象,他的对外接口只是一种静态定义,在代码静态解析阶段就会生成。
第一个差异:
CommonJS模块输出的是值的复制,一旦输出一个值,模块内部的变化就影响不到这个值了。
//main var mod = require("./commonop"); console.log(mod.counter); //3 mod.incCounter(); console.log(mod.counter); //3 //commonop var counter = 3; function incCounter() { counter++; } module.exports = { counter : counter, incCounter:incCounter }
ES6模块的运行机制与commonJS不一样,JS引擎对脚本静态分析的时候,遇到模块加载命令import就会生成一个只读引用。等到脚本真正执行时,再根据这个只读引用到被加载的模块中取值。
import { counter,incCounter } from "./moduleop" console.log(counter); //3 incCounter(); console.log(counter); //4 //moduleop export let counter = 3; export function incCounter() { counter++; }
-
循环加载
-
CommonJS模块的加载原理
CommonJS的一个模块就是一个脚本文件。require命令第一次加载该脚本时就会执行整个脚本,然后在内存中生成一个对象。
CommonJS模块的重要特性是加载时执行,即脚本代码在require的时候全部执行,一旦出现某个模块被”循环加载”,就只输出已经执行的部分,还未执行的部分不会输出。
//commonip exports.done = false; var op = require("./commonop.js"); console.log("在commonop中op.done为:",op.done); exports.done = true; console.log("commonip.js执行完毕!"); //commonip exports.done = false; var ip = require("./commonip.js"); console.log("在commonop中ip.done为:",ip.done); exports.done = true; console.log("commonop.js执行完毕!"); //输出 //在commonop中op.done为:false //commonip.js执行完毕! //在commonop中ip.done为:true //commonop.js执行完毕!
CommonJS执行过程类似函数执行
-
ES6模块的加载
//moduleop import { bar } from "./moduleip"; console.log("moduleop"); console.log(bar); export let foo = "foo"; //moduleip import { foo } from "./moduleop"; console.log("moduleip"); console.log(foo); export var bar = "bar"; //输出 //moduleip //undefined //moduleop //bar
moduleop的第一行就是执行加载moduleip,而moduleip第一行也是执行moduleop,但是moduleop已经开始执行了,所以不会重复执行,而是继续执行moduleip
-
第24章 编程风格
-
let取代var
-
全局变量和线程安全
在let和const之间,建议优先使用const,尤其是在全局环境中,不应该设置变量
const优于let的原因
- const可以提醒阅读程序的人,这个变量不应该改变
- const比较符合函数式编程思想,运算不改变值,只是新建值,而且这样也有利于将来的分布式运算
- js编译器会对const进行优化,所以多使用const有利于提供程序的运行效率。也就是说let和const的本质区别是编译器内部的处理不同
所有的函数有应该设置为常量
-
字符串
- 静态字符串一律使用单引号或者反引号
- 动态字符串使用反引号
-
解构赋值
- 使用数组成员对变量赋值时,优先使用解构赋值
- 函数的参数如果是对象,优先使用解构赋值
- 如果函数返回的是多个值,优先使用对象的解构赋值,而不是数组的解构赋值。
-
对象
- 单行定义的对象,最后一个成员不以逗号结尾,多行定义的对象,最后一个成员以逗号结尾
- 对象尽量静态化,一旦定义,就不得随意添加新的属性。如果添加属性不可避免,要使用Object.assgin方法
-
数组
- 使用扩展运算符(…)复制数组
- 使用Array.from将类似数组的对象转为数组
-
函数
-
立即执行的函数可以写成箭头函数的形式
(()=>{ console.log("test"); })();
-
尽量使用箭头函数,还可以顺便绑定this
-
不要在函数体内使用arguments变量,使用rest运算符(…)代替。因为rest运算符可以显示表明我们想要获取的参数,而且arguments是一个类似数组的对象,而rest运算符可以提供一个真正的数组
-
使用默认值语法设置函数参数的默认值
-
-
注意区分Object和Map,只有模拟实体对象时,才使用Object,如果只是需要key:value的数据结构,则使用Map。Map有内建的遍历机制
-
总是使用Class取代需要prototype的操作,因为class的写法更简洁,更易理解
-
模块
- 如果模块只有一个输出值,就使用export default
- 不要同时使用export和export default
第26章 ArrayBuffer
-
arraybuffer对象代表二进制数据的一段内存,它不能直接读写,必须通过视图操作,视图的作用就是以指定格式解读二进制数据
let buf = new ArrayBuffer(32); let dataview = new DataView(buf); console.log(dataview.getInt8(0));//0 //上面的代码对一段32字节的内存建立dataview视图,然后以不带符号的8位整数格式读取第一个元素,结果得到0,因为原始内存的arraybuffer对象默认所有位都是0
-
typearray与dataview的一个区别是,它不是一个构造函数,是一组构造函数
let buf = new ArrayBuffer(12); let x1 = new Int8Array(buf); let x2 = new Uint8Array(buf); x1[0] = 1; console.log(x2[0]);//1
上面的代码对同一段内存构造了两种视图,由于两个视图对应的是同一段内存,因此一个视图修改底层内存会影响到另一个视图
-
slice方法可以将内存区域的一部分复制生成一个新的arraybuffer对象
var buf = new ArrayBuffer(8); var newbuf = buf.slice(0,3);
复制buffer对象的前3个字节生成一个新的arraybuffer对象。slice方法其实包含两步,第一步先分配一段新内存,第二步将原来那个arraybuffer对象复制过去
-
普通数组和typedarray的差异
- typedarray数组的所有成员都是同一种类型
- typedarray数组是连续的,不会有空位
- typedarray数组成员默认值是0。比如new array(10)返回一个普通数组,里面没有任何成员,只有10个空位
- typedarray数组只是一层视图,本身不储存数据,它的数据都存储在底层的arraybuffer里,要获得对象必须使用buffer属性
-
TypedArray数组的构造函数可以接受另一个TypedArray实例作为参数
此时生成的新数组只是复制了参数数组的值,对应的底层内存是不一样的。新数组会开辟一段新的内存存储数据,不会在原数组的内存之上建立视图
如果想基于同一段内存构造不同的视图可以利用对象的buffer属性
let x = new Int8Array([1,1]); let y = new Int8Array(x.buffer); x[0] = 2; console.log(y[0]);//2
-
注意字节序(大小端)
-
与普通数组相比,typearray数组的最大优点就是可以直接操作内存,不需要数据类型转化,所以速度快很多
const l = 10000000; let data1 = new ArrayBuffer(l); if(data1.byteLength != l) console.log("内存分配失败!"); else{ let view = new Uint8Array(data1); view[0] = view[1] = 1; const p1 = (new Date()).valueOf(); for(let i = 2;i<l;i++) view[i] = view[i-1]+view[i-2]; const p2 = (new Date()).valueOf(); console.log(p2-p1); //29 } (()=>{ let a = new Array(l); a[0] = a[1] = 1; const p1 = (new Date()).valueOf(); for(let i = 2;i<l;i++) a[i] = a[i-1]+a[i-2]; const p2 = (new Date()).valueOf(); console.log(p2-p1); //84 })();
-
考虑溢出
普通溢出就是取后n位(n决定于视图类型)
但是Uint8ClampedArray视图溢出规则与上面的规则不同,它规定凡是正向溢出,该项一律等于最大值即255,逆向溢出一律等于最小值0。
-
set
typedarray数组的set方法用于复制数组也就是将一段内存完全复制到另一段内存
var a = new ArrayBuffer(8); var b = new ArrayBuffer(10); b.set(a);
set是整段内存的复制,比一个个成员的复制快的多
-
静态方法from接受一个可以遍历的数据结构作为参数,返回一个基于此结构的typedarray实例,并且还可以接受第二个参数,用来对每个元素进行遍历
let a = Int16Array.from(Int8Array.of(127,126,125),x=>x*2); console.log(a); //Int16Array [ 254, 252, 250 ]
from没有发生溢出,这说明遍历不是针对原来的8位整数数组,也就是说,from会将第一个参数指定的typearray数组复制到另一段内存之中,处理之后再将结果转成指定的数据格式
-
默认情况下,dataview的get方法使用大端解读数据,如果需要小端解读,可以在第二个参数指定true