入门与进阶
第一章 进入编程
1,如何快速运行一段js代码?
方式1:浏览器开发者工具。可以使用快捷键option+command+J或者菜单选项开发者工具来启动开发者控制台。单选直接输入,多行按shift+enter切换行
方式2:Chrome的snippets是小脚本,还可以创作并在Chrome DevTools的来源面板中执行。 首先通过f12 打开开发者工具,再打开Sources面板中,单击上Snippets选项卡,在导航器中单击鼠标右键,然后选择New。
方式3:node脚本。运行node xxx.js,即可执行该js文件
方式4:在线js编辑器。https://codepen.io/ https://jsfiddle.net/
方式5:建demo项目,这种方式可以支持第三方库
2,常量命名习惯
//常量通常是大写的,在多个单词之间使用下划线_连接
const TAX_RATE = 0.08;
1、JS严格区分大小写,test和Test是两个变量。
2、命名遵循`驼峰命名法。
3、可以使用数字、字母、下划线、$来命名,但是数字不能作为名字的开始,也不支持中杠(-)
4、_开头的变量是公共变量(全局变量)(var _student)
5、常量通常是大写的,在多个单词之间使用下划线_连接
6、不能使用关键字和保留字命名;
关键字:在JS中有特殊含义的,例如:var、for、break、continue…
保留字:未来可能会成为关键字的,例如:class
第二章 进入JavaScript
1,如何比较两个值
==在允许强制转换的条件下检查值的等价性,而===是在不允许强制转换的条件下检查值的等价性;因此===常被称为“严格等价”。
var a = "42";
var b = 42;
a == b; // true
a === b; // false
即 == 先将两者强制转换为同一种类型再进行比较。
如果你在比较两个非基本类型值,比如object(包括function和array),它比较的是引用是否相同,而不是它们底层的值。
var a = [1,2,3];
var b = [1,2,3];
var c = "1,2,3";
a == c; // true
b == c; // true
a == b; // false
如果是不等价比较:<,>,<=,和≥,比较的两个值也是先强制转换,它使用典型的字母顺序规则("bar" < "foo",如下,
var a = 41;
var b = "42";
var c = "43";
a < b; // true
b < c; // true
需要注意的是,如果一个值是数字,另一个强制转换为数字,如下,值b被强制转换为了“非法的数字值”NaN
var a = 42;
var b = "foo";
a < b; // false
a > b; // false
a == b; // false
总结一下JS中经常遇到纯数字和各种各样的字符串进行比较:
- 纯数字之间的比较
alert(1<3);//true
- 数字字符串比较,会将其先转成数字
alert("1"<"3");//true
alert("123"<"123");//false
- 纯字符串比较,先转成ascii码
alert("a"<"b");//true
alert("abc"<"aad");//false,多纯字母比较,会依次比较ascii码
- 汉字比较
alert("我".charCodeAt());//25105
alert("的".charCodeAt());//30340
alert("我"<"的");//true,汉字比较,转成ascii码
- 当数字和字符串比较,且字符串为数字。则将数字字符串转为数字
alert(123<"124");//true,下面一句代码得出124的ascii码为49,所以并不是转成ascii比较
alert("124".charCodeAt());//49
- 当数字和字符串比较,且字符串为非纯数字时,则将非数字字符串转成数字的时候会转换为NaN,当NaN和数字比较时不论大小都返回false.
alert(13>"abc");//false
作用域与闭包
1,需要弃用 var
改用 let
么?
是的,在 ES6 不要使用 var,用 let 或 const 代替。
https://www.zhihu.com/question/34294629
后续会增加js检验规则,全面禁用var
第一章 什么是作用域?
1,什么是作用域
它是一组明确定义的规则,它定义如何在某些位置存储变量,以及如何在稍后找到这些变量。换句话说,变量被存储在哪儿?而且,最重要的是,我们的程序如何在需要它们的时候找到它们
2,js 编译过程是怎样的?
编译程序一般步骤分为:词法分析、语法分析、语义检查、代码优化和生成字节码。
分词/词法分析(Tokenizing/Lexing)。就好比我们将一句话,按照词语的最小单位进行分割。例如,考虑程序 var a=2。这段程序通常会被分解成为下面这些词法单元:var,a,=,2;空格是否作为当为词法单位,取决于空格在这门语言中是否具有意义。
解析/语法分析(Parsing)。将词法单元流转换成一个由元素逐级嵌套所组成的代表了程序语法结构的树。这个树称为“抽象语法树”(Abstract Syntax Tree,AST)。词法分析和语法分析不是完全独立的,而是交错进行的,也就是说,每取得一个词法记号,就将其送入语法分析器进行分析。举例说明
if (typeof a == "undefined") {
a = 0;
} else {
a = a;
}
alert(a);
image
代码生成。将 AST 转换成可执行代码的过程被称为代码生成。这个过程与语言、目标平台相关。
推荐另一篇文章,介绍的很详细。https://juejin.im/post/5c2ca1106fb9a049ac794510
推荐一个动手项目,自己写一个js解释器:https://juejin.im/entry/5c0538245188257c3045ccc3
3,ReferenceError和TypeError分别表示什么错误?
调用一个“未声明”的变量,在作用域中找不到,会报 ReferenceError。
变量被找到了,但是你试着去做一些这个值不可能做到的事,比如将一个非函数的值作为函数运行,或者引用 null 或者 undefined 值的属性,那么 引擎 就会抛出一个不同种类的错误,称为 TypeError。
第二章 词法作用域
1,查询变量作用域的过程是怎样的
首先从最内部的作用域开始,如果找不到,则向上走一层,到外层最近的作用域。一旦找到第一个匹配的,查询就停止了。
image在上面的代码段中,引擎 执行语句 console.log(..) 并开始查找三个被引用的变量 a,b 和 c。它首先从最内部的作用域气泡开始,也就是 bar(..) 函数的作用域。在这里它找不到 a,所以它向上走一层,到外面下一个最近的作用域气泡,foo(..) 的作用域。它在这里找到了 a,于是它就使用这个 a。同样的事情也发生在 b 身上。但是对于 c,它在 bar(..) 内部就找到了。
2,怎样欺骗词法作用域?
eval(..)、setTimeout(..)、setInterval(..)、with,都可以
以eval为例说明,eval(..) 函数接收一个字符串作为参数值,运行时动态翻译成执行代码
function foo(str, a) {
eval( str ); // 作弊!
console.log( a, b );
}
var b = 2;
foo( "var b = 3;", 1 ); // 1 3
eval(..) 和 with 都可以欺骗编写时定义的词法作用域,代价是让代码优化失去作用,影响代码执行速度
第三章 函数与块儿作用域
1,什么函数作用域
函数作用域的含义是指,属于这个函数的全部变量都可以在整个函数的范围内使用及复用(事实上在嵌套的作用域中也可以使用)。这种设计方案是非常有用的,能充分利用JavaScript变量可以根据需要改变值类型的“动态”特性。
function foo(a) {
var b = 2;
// 一些代码
function bar() {
// ...
}
// 更多代码
var c = 3;
}
bar(); // 失败
console.log( a, b, c ); // 3个都失败
2,什么是块作用域
块作用域指的是变量和函数不仅可以属于所处的作用域,也可以属于某个代码块(通常指{ .. }内部)。
3,let关键字的作用
let关键字(var关键字的表亲),用来在任意代码块中声明变量。if (..) { let a = 2; }会声明一个劫持了if的{ .. }块的变量,并且将变量添加到这个块中
第4章 提升
1,为什么作用域会提升
变量和函数在内的所有声明都会在任何代码被执行前首先被处理,换句话说声明本身会被提升,而赋值或其他运行逻辑会留在原地。
a = 2;
var a;
console.log( a );// 2
//函数声明会被提升,就像我们看到的。但是函数表达式不会。
foo();
function foo() {
console.log( a ); // undefined
var a = 2;
}
尽量避免在同一个作用域中重复定义,经常会导致各种奇怪的问题
第5章 闭包
1,什么是闭包
闭包是指有权访问另一个函数作用域中的变量的函数。闭包保存了外部函数的作用域链中的变量对象,本质上,闭包就是将函数内部和函数外部连接起来的一座桥梁
闭包的最大用处有两个,一个是可以读取函数内部的变量,另一个就是让这些变量的值始终保持在内存中。
闭包介绍:https://www.ruanyifeng.com/blog/2009/08/learning_javascript_closures.html
2,js中的闭包跟ios中的闭包/block是同一个东西吗
不是,ios中的闭包是一个回调函数,而js中的闭包是能够读取其他函数内部变量的函数,通常用于封装私有变量,将子函数中的私有变量提供出来,让外层函数能够访问
// swift 闭包
outText(callback: { (text: String) in
print(text)
})
// 对应的js回调函数
outText((text) => {
console.log(text)
})
this与对象原型
第1章 关于this
1,js中this的原理
this是在运行时进行绑定的,并不是在编写时绑定,它的上下文取决于函数调用时的各种条件。this的绑定和函数声明的位置没有任何关系,只取决于函数的调用方式。
this实际上是在函数被调用时发生的绑定,它指向什么完全取决于函数在哪里被调用。
https://www.ruanyifeng.com/blog/2018/06/javascript-this.html
2,为什么要用this
this提供了一种更优雅的方式来隐式“传递”一个对象引用,因此可以将API设计得更加简洁并且易于复用。
第2章 this全面解析
1,this绑定优先级
现在我们可以根据优先级来判断函数在某个调用位置应用的是哪条规则。可以按照下面的顺序来进行判断:
1.函数是否在new中调用(new绑定)?如果是的话this绑定的是新创建的对象。
var bar = new foo()
2.函数是否通过call、apply(显式绑定)或者硬绑定调用?如果是的话,this绑定的是指定的对象。
var bar = foo.call(obj2)
3.函数是否在某个上下文对象中调用(隐式绑定)?如果是的话,this绑定的是那个上下文对象。
var bar = obj1.foo()
4.如果都不是的话,使用默认绑定。如果在严格模式下,就绑定到undefined,否则绑定到全局对象。
var bar = foo()
就是这样。对于正常的函数调用来说,理解了这些知识你就可以明白this的绑定原理了。
2,箭头函数有什么用
箭头函数不使用this的四种标准规则,而是根据外层(函数或者全局)作用域来决定this。
箭头函数可以像bind(..)一样确保函数的this被绑定到指定对象,此外,其重要性还体现在它用更常见的词法作用域取代了传统的this机制。
第3章 对象
1,什么是对象,为什么我们需要指向它们?
对象就是键/值对的集合。它是六种基本类型之一,对象有包括function在内的子类型,不同子类型具有不同的行为,比如内部标签[object Array]表示这是对象的子类型数组。
2,内置对象与对象有什么关系?
内置对象是一些对象子类型,如下
· String· Number· Boolean· Object· Function· Array· Date· RegExp· Error
这些内置函数可以当作构造函数(由new产生的函数调用——参见第2章)来使用,从而可以构造一个对应子类型的新对象。举例来说:
var strPrimitive = "I am a string";
typeof strPrimitive; // "string"
strPrimitive instanceof String; // false
var strObject = new String( "I am a string" );
typeof strObject; // "object"
strObject instanceof String; // true
// 考察 object 子类型
Object.prototype.toString.call( strObject ); // [object String]
我们在字符串的基本类型上调用属性和方法,引擎会自动地将它强制转换为 String 对象。
基本类型值 "I am a string" 不是一个对象,它是一个不可变的基本字面值。为了对它进行操作,比如检查它的长度,访问它的各个独立字符内容等等,都需要一个 String 对象。
JS 社区的绝大部分人都 强烈推荐 尽可能地使用字面形式的值,而非使用构造的对象形式。
3,如何动态访问对象内容
操作符要求属性名满足标识符的命名规范,而[".."]语法可以接受任意UTF-8/Unicode字符串作为属性名。举例来说,如果要引用名称为"Super-Fun! "的属性,那就必须使用["Super-Fun! "]语法访问,因为Super-Fun!并不是一个有效的标识符属性名。此外,由于[".."]语法使用字符串来访问属性,所以可以在程序中构造这个字符串,比如说:
var wantA = true;
var myObject = {
a: 2
};
var idx;
if (wantA) {
idx = "a";
}
// 稍后
console.log( myObject[idx] ); // 2
4,js 中有几种遍历方法,他们有什么区别?
for 语句、forEach语句、for-in 语句、for-of 语句 (ES 6)
for 语句是标准的循环写法,定义一个变量i作为索引,以跟踪访问的位置,len是数组的长度,条件就是i不能超过len。
forEach 方法对数组的每个元素执行一次提供的CALLBACK函数,forEach是一个数组方法,可以用来把一个函数套用在一个数组中的每个元素上,forEach为每个数组元素执行callback函数只可用于数组
var arr = [1,5,8,9]
arr.forEach(function(item) {
console.log(item);
})
一般会使用for-in来遍历对象的属性的,不过属性需要 enumerable,才能被读取到. for-in 循环只遍历可枚举属性。
var obj = {
name: 'test',
color: 'red',
day: 'sunday',
number: 5
}
for (var key in obj) {
console.log(obj[key])
}
for-of
语句在可迭代对象(包括 Array,Map,Set,String,TypedArray,arguments 对象等等)上创建一个迭代循环,调用自定义迭代钩子,并为每个不同属性的值执行语句。只要是一个iterable的对象,就可以通过for-of
来迭代.
var arr = [{name:'bb'},5,'test']
for (item of arr) {
console.log(item)
}
性能对比,如下
for > for-of > forEach > filter > map > for-in
详细资料:https://juejin.im/post/5a3a59e7518825698e72376b#heading-14
第4章 混合对象“类”
1,JS是面向对象编程还是面向过程编程?JS拥有类吗?
JS 虽然有近似类的语法,但是JavaScript的机制似乎一直在阻止你使用类设计模式。在近似类的表象之下,JavaScript的机制其实和类完全不同。语法糖和(广泛使用的)JavaScript“类”库试图掩盖这个现实,但是你迟早会面对它:其他语言中的类和JavaScript中的“类”并不一样。
2,为什么会有"类"设计模式的出现?
类是一种设计模式,类/继承描述了一种代码的组织结构形式——一种在软件中对真实世界中问题领域的建模方法。
3,js 中的继承、多态、多重继承
在继承或者实例化时,JavaScript的对象机制并不会自动执行复制行为。简单来说,JavaScript中只有对象,并不存在可以被实例化的“类”。一个对象并不会被复制到其他对象,它们会被关联起来(参见第5章)。由于在其他语言中类表现出来的都是复制行为,因此JavaScript开发者也想出了一个方法来模拟类的复制行为,这个方法就是混入。
// 大幅简化的 `mixin(..)` 示例:
function mixin( sourceObj, targetObj ) {
for (var key in sourceObj) {
// 仅拷贝非既存内容
if (!(key in targetObj)) {
targetObj[key] = sourceObj[key];
}
}
return targetObj;
}
var Vehicle = {
engines: 1,
ignition: function() {
console.log( "Turning on my engine." );
},
drive: function() {
this.ignition();
console.log( "Steering and moving forward!" );
}
};
var Car = mixin( Vehicle, {
wheels: 4,
drive: function() {
Vehicle.drive.call( this );
console.log( "Rolling on all " + this.wheels + " wheels!" );
}
} );
Car 现在拥有了一份从 Vehicle 得到的属性和函数的拷贝。技术上讲,函数实际上没有被复制,而是指向函数的 引用 被复制了。所以,Car 现在有一个称为 ignition 的属性,它是一个 ignition() 函数引用的拷贝;而且它还有一个称为 engines 的属性,持有从 Vehicle 拷贝来的值 1。Car已经 有了 drive 属性(函数),所以这个属性引用没有被覆盖(参见上面 mixin(..) 的 if 语句)。
类意味着拷贝。
当一个传统的类被实例化时,就发生了类的行为向实例中拷贝。当类被继承时,也发生父类的行为向子类的拷贝。
多态(在继承链的不同层级上拥有同名的不同函数)也许看起来意味着一个从子类回到父类的相对引用链接,但是它仍然只是拷贝行为的结果。
JavaScript 不会自动地 (像类那样)在对象间创建拷贝。
mixin 模式常用于在 某种程度上 模拟类的拷贝行为,但是这通常导致像显式假想多态那样(OtherObj.methodName.call(this, ...)
)难看而且脆弱的语法,这样的语法又常导致更难懂和更难维护的代码。
明确的 mixin 和类 拷贝 又不完全相同,因为对象(和函数!)仅仅是共享的引用被复制,不是对象/函数自身被复制。不注意这样的微小之处通常是各种陷阱的根源。
第5章 原型
参考资料:https://mp.weixin.qq.com/s/fMvSims4VBeoKs0JJhJYtA
-
JS Prototype 原型对应的数据结构和算法是什么?
JS 原型其实是一个隐式的单向链表。prototype 除了不叫 next,以及是一个隐式引用外,跟下面的单向链表结构如出一辙。
image
-
什么是 js 原型
从数据结构的角度看,js 原型是一个以隐式引用作为存储方式,以点操作符和属性访问语句作为语法糖的单向链表。并且,原型链并没有发挥出单向链表的全部能力。大部分情况下,只用到了 addFirst 这个操作(即原型继承)。极少场景使用 addLast, traversing, insertBefore, insertAfter 等链表操作。
JS 原型是指为其它对象提供共享属性访问的对象。在创建对象时,每个对象都包含一个隐式引用指向它的原型对象或者 null。
-
什么是原型链
原型也是对象,因此它也有自己的原型。如果在第一个对象上没有找到需要的属性或者方法引用,引擎就会继续在[[Prototype]]关联的对象上进行查找。同理,如果在后者中也没有找到需要的引用就会继续查找它的[[Prototype]],以此类推。这一系列对象的链接被称为“原型链”。
-
js 中的 new 是什么
那么在javaScript中new到底做了什么呢?其实当执行 var o = new Foo(); 时,javaScript实际执行如下(或类似):
var o = new Object(); o.__proto__ = Foo.prototype; Foo.call(o); //原型链为: // o ---> Function.prototype ---> Object.prototype ---> null
所以new是什么?它实际上就是新生成一个对象o,然后把这个对象的prototype链到了Foo.prototype对象上。
-
Object.setPropertyOf 和 Object.create 的差别
1)Object.setPropertyOf,给我两个对象,我把其中一个设置为另一个的原型。
2)Object.create,给我一个对象,它将作为我创建的新对象的原型。
-
类和原型的区别
基于 class 的继承,继承的是行为和结构,但没有继承数据。
而基于 prototype 的继承,可以继承数据、结构和行为三者。
-
js 原型有哪些问题
难以理清我访问的属性和方法,来自原型链的哪一个对象。我们需要手动判断 key 是否属于 obj 自身,然后进行真正的操作。因此,有一些开发者,建议不用 for in,总是使用 Object.keys。Object.keys 将 obj 自身包含的所有可遍历的 key,装配成数组形式返回。
此外在原型上追加数据和方法,会影响到所有继承该原型的对象。如挂载到 Object, Array, Number 等全局构造函数的原型上,将使所有代码都变得更不可靠。
第6章 行为委托
-
instanceof有什么问题,有没有替代方法
使用Bar instanceof Foo(因为很容易把“实例”理解成“继承”),但是在JavaScript中这是行不通的,你必须使用Bar.prototype instanceof Foo。
我们不再使用 instanceof,因为它令人迷惑地假装与类有关系。现在,我们只需要(非正式地)问这个问题,“你是我的 一个 原型吗?”。不再需要用 Foo.prototype 或者痛苦冗长的 Foo.prototype.isPrototypeOf(..) 来间接地查询了。
// `Foo` 和 `Bar` 互相的联系 Foo.isPrototypeOf( Bar ); // true Object.getPrototypeOf( Bar ) === Foo; // true // `b1` 与 `Foo` 和 `Bar` 的联系 Foo.isPrototypeOf( b1 ); // true Bar.isPrototypeOf( b1 ); // true Object.getPrototypeOf( b1 ) === Bar; // true
类型和语法
第1章 类型
-
null 检测
null 有个存在20年的bug,如果要检测 null,可以使用复合条件
var a = null; (!a && typeof a === "object"); // true
-
什么是undefined
变量在未持有值的时候为undefined。此时typeof返回"undefined"
var a; typeof a; // "undefined" var b = 42; var c; // 稍后 b = c; typeof b; // "undefined" typeof c; // "undefined"
第2章 值
-
数组注意事项
若有空缺单元时要注意,会默认填充一个undefined的值
var a = [ ]; a[0] = 1; // 这里没有设置值槽 `a[1]` a[2] = [ 3 ]; a[1]; // undefined a.length; // 3
数组是对象的一种,因此也包含字符串键值和属性(但这些并不计算在数组长度内),最好不要当对象来使用。
如果字符串键值是个数字,会被当成数字索引处理
var a = [ ]; a["13"] = 42; a.length; // 14
-
数字注意事项
JavaScript没有真正意义上的整数,JavaScript中的“整数”就是没有小数的十进制数。所以42.0即等同于“整数”42。
在处理带有小数的数字时需要特别注意。很多(也许是绝大多数)程序只需要处理整数,最大不超过百万或者万亿,此时使用JavaScript的数字类型是绝对安全的
0.1 + 0.2 === 0.3; // false
小数点解决方法:把小数转成整数后再运算
https://github.com/camsong/blog/issues/9
有时JavaScript程序需要处理一些比较大的数字,如数据库中的64位ID等。由于JavaScript的数字类型无法精确呈现64位数值,所以必须将它们保存(转换)为字符串。
整数检测
Number.isInteger( 42 ); // true Number.isInteger( 42.000 ); // true Number.isInteger( 42.3 ); // false
-
特殊数值注意事项
undefined类型只有一个值,即undefined。null类型也只有一个值,即null。它们的名称既是类型也是值。
undefined和null常被用来表示“空的”值或“不是值”的值。二者之间有一些细微的差别。例如:
• null指空值(empty value)
• undefined指没有值(missing value)
或者:
• undefined指从未赋值
• null指曾赋过值,但是目前没有值
null是一个特殊关键字,不是标识符,我们不能将其当作变量来使用和赋值。然而undefined却是一个标识符,可以被当作变量来使用和赋值。
-
特殊的数字注意事项
NaN意指“不是一个数字”(not a number)
var a = 2 / "foo"; // NaN typeof a === "number"; // true
NaN是一个特殊值,它和自身不相等,可以使用ES6中的工具函数Number.isNaN(..)来判断一个值是否是NaN。
var a = 2 / "foo"; a == NaN; // false a === NaN; // false Number.isNaN( a ); // true Number.isNaN( 'foo' ); // true
负零的检测
function isNegZero(n) { n = Number( n ); return (n === 0) && (1 / n === -Infinity); } isNegZero( -0 ); // true isNegZero( 0 / -3 ); // true isNegZero( 0 ); // false
ES6中新加入了一个工具方法Object.is(..)来判断两个值是否绝对相等,可以用来处理上述所有的特殊情况:
var a = 2 / "foo"; var b = -3 * 0; Object.is( a, NaN ); // true Object.is( b, -0 ); // true Object.is( b, 0 ); // false
Object.is 详细资料:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/is
关于 == vs === vs Object.is,结论是这三个比较运算符都非常有用,当进行一些普遍的比较时,则推荐使用 ===,而不是 ==;当遇到 NaN、0、+0、-0较为特殊值比较时,推荐使用Object.is。
-
值和引用注意事项
简单值(即标量基本类型值,scalar primitive)总是通过值复制的方式来赋值/传递,包括null、undefined、字符串、数字、布尔和ES6中的symbol。
复合值(compound value)——对象(包括数组和封装对象,参见第3章)和函数,则总是通过引用复制的方式来赋值/传递。
var a = 2; var b = a; // `b` 总是 `a` 中的值的拷贝 b++; a; // 2 b; // 3 var c = [1,2,3]; var d = c; // `d` 是共享值 `[1,2,3]` 的引用 d.push( 4 ); c; // [1,2,3,4] d; // [1,2,3,4]
JavaScript中的引用和其他语言中的引用/指针不同,它们不能指向别的变量/引用,只能指向值。
var a = [1,2,3]; var b = a; a; // [1,2,3] b; // [1,2,3] // 稍后 b = [4,5,6]; a; // [1,2,3] b; // [4,5,6]
JavaScript中的引用和其他语言中的引用/指针不同,它们不能指向别的变量/引用,只能指向值
1. function foo(x){ // x.push(4); x; //(4) [1, 2, 3, 4] x = [4,5,6]; //x =[4,5,6] 该赋值并不影响a的指向,所以a仍然指向[1,2,3,4] x.push(7); x;//(4) [4, 5, 6, 7] } var a = [1,2,3]; foo(a); // a; //(4) [1, 2, 3, 4] 2. function foo(x){ x.push(4); x; //(4) [1, 2, 3, 4] x.length = 0; //不能通过引用x来更改引用a的指向,只能更改a和x共同指向的值, x.push(4,5,6,7); x; //(4) [4, 5, 6, 7] } var a = [1,2,3]; foo(a);
第3章 原生函数
-
js中的封装对象包装
JavaScript会自动为基本类型值包装(box或者wrap)一个封装对象,比如"abc",如果要访问它的length属性或String.prototype方法,JavaScript引擎会自动对该值进行封装(即用相应类型的封装对象来包装它)来实现对这些属性和方法的访问:
var a = "abc"; a.length; // 3 a.toUpperCase(); // "ABC"
一般情况下,我们不需要直接使用封装对象。最好的办法是让JavaScript引擎自己决定什么时候应该使用封装对象。即应该优先考虑使用"abc"和42这样的基本类型值,而非new String("abc")和new Number(42)。
Date(..)和Error(..),没有对应的常量形式来作为它们的替代。
拆封,valueOf(): 得到封装对象中的基本类型值
var a = new String("a") var b = new Number(43) var c = new Boolean(true) a.valueOf() //"a" b.valueOf() //43 c.valueOf() //true
第4章 强制类型转换
-
toString()
基本类型值的字符串化规则为:null转换为"null", undefined转换为"undefined", true转换为"true"。数字的字符串化则遵循通用规则,极小和极大的数字使用指数形式.
对普通对象来说,除非自行定义,否则toString()(Object.prototype.toString())返回内部属性[[Class]]的值(参见第3章),如"[object Object]"。
数组在做字符串化时,将数组所有元素字符串化再用","连接。
工具函数JSON.stringify(..)在将JSON对象序列化为字符串时也用到了ToString。JSON.stringify(..)在对象中遇到undefined、function和symbol时会自动将其忽略,在数组中则会返回null(以保证单元位置不变)。
-
toNumber()
将非数字值当作数字来使用,比如数学运算。其中true转换为1, false转换为0。undefined转换为NaN, null转换为0。处理失败时返回NaN。
parseInt() 和 parseFloat()
console.log(parseInt('a')) // NaN console.log(parseInt('11')) // 11 console.log(parseInt('11aa')) // 11 console.log(parseInt('0xf')) // 15 console.log(parseFloat('12.3a')) // 12.3 console.log(parseFloat('0xf')) // 0 console.log(parseFloat('01.1')) // 1.1
-
toBoolean
console.log(Boolean("0")) // true console.log(Boolean([])) // true console.log(Boolean(undefined)) // false console.log(Boolean(null)) // false console.log(!!"0") // true console.log(!![]) // true console.log(!!undefined) // false console.log(!!null) // false
-
日期显示转换为数字
一元运算符+的另一个常见用途是将日期(Date)对象强制类型转换为数字,返回结果为Unix时间戳,以毫秒为单位(从1970年1月1日00:00:00 UTC到当前时间):
不建议对日期类型使用强制类型转换,推荐用Date.now();
var d = new Date( "Mon, 18 Aug 2014 08:53:06 CDT" ); +d; // 1408369986000 或者 var timestamp = +new Date(); 或者 var timestamp = new Date().getTime(); 或者(推荐) var timestamp = Date.now();
-
显式转换为布尔值
建议使用Boolean(a)和!! a来进行显式强制类型转换。
var a = "0"; var b = []; var c = {}; var d = ""; var e = 0; var f = null; var g; !!a; // true !!b; // true !!c; // true !!d; // false !!e; // false !!f; // false !!g; // false Boolean( a ); // true Boolean( b ); // true Boolean( c ); // true Boolean( d ); // false Boolean( e ); // false Boolean( f ); // false Boolean( g ); // false
|| 和 &&
和其他语言不同,在JavaScript中它们返回的并不是布尔值。
它们的返回值是两个操作数中的一个(且仅一个),即选择两个操作数中的一个,然后返回它的值。
var a = 42; var b = "abc"; var c = null; a || b; //42 a && b; //"abc" c || b; //"abc" c && b; //null
异步和性能
第1章 异步
-
分块的程序
实际上,JavaScript程序总是至少分为两个块:第一块现在运行;下一块将来运行,以响应某个事件。尽管程序是一块一块执行的,但是所有这些块共享对程序作用域和状态的访问,所以对状态的修改都是在之前累积的修改之上进行的。
最常见的块单位是函数。现在无法完成的任务会以回调函数在将来完成。
任何时候,只要把一段代码包装成一个函数,并指定它在响应某个事件(定时器、鼠标点击、Ajax响应等)时执行,这就是异步机制。
-
事件循环
有一个用while循环实现的持续运行的循环,事件循环的每一轮称为一个 tick。 用户交互、IO 和定时器会向事件队列中加入事件。一旦有事件进入, 事件循环就会运行, 直到队列清空。任意时刻,一次只能从队列中处理一个事件。
// `eventLoop`是一个像队列一样的数组(先进先出) var eventLoop = [ ]; var event; // “永远”执行 while (true) { // 执行一个"tick" if (eventLoop.length > 0) { // 在队列中取得下一个事件 event = eventLoop.shift(); // 现在执行下一个事件 try { event(); } catch (err) { reportError(err); } } }
-
并发
js 一次只能处理一个事件,两个或多个进程同时执行就出现了并发。并发是指两个或多个事件链随时间发展交替执行,以至于从更高的层次来看,就像是同时 在运行(尽管在任意时刻只处理一个事件)。
如果进程间没有相互影响,不确定性是完全可以接受的。
如果并发的进程需要相互交流,需要对它们进行协调以避免竞态的出现。如下
var a, b; function foo(x) { a = x * 2; if (a && b) { baz(); } } function bar(y) { b = y * 2; if (a && b) { baz(); } } function baz() { console.log( a + b ); } // ajax(..) 是某个包中任意的Ajax函数 ajax( "http://some.url.1", foo ); ajax( "http://some.url.2", bar );
baz()调用周围的if (a && b)条件通常称为“大门”,因为我们不能确定a和b到来的顺序,但在打开大门(调用baz())之前我们等待它们全部到达。
-
任务
任务队列,是挂在事件循环队列的每个tick之后的一个队列。在事件循环的每个tick中,可能出现的异步动作不会导致一个完整的新事件添加到事件循环队列中,而会在当前tick的任务队列末尾添加一个项目(一个任务)。
事件循环队列类似于一个游乐园游戏:玩过了一个游戏之后,你需要重新到队尾排队才能再玩一次。而任务队列类似于玩过了游戏之后,插队接着继续玩。
console.log( "A" ); setTimeout( function(){ console.log( "B" ); }, 0 ); // 理论上的 "Job API" schedule( function(){ console.log( "C" ); schedule( function(){ console.log( "D" ); } ); } );
它将会打出A C D B,因为Job发生在当前的事件轮询tick的末尾,而定时器会在 下一个 事件轮询tick触发。
-
语句顺序
代码中语句的顺序和JavaScript引擎执行语句的顺序并不一定要一致。编译器语句重排序几乎就是并发和交互的微型隐喻。
第2章 回调
回调函数是 JavaScript 异步的基本单元。但是随着 JavaScript 越来越成熟,对于异步编程领域的发展,回调已经不够用了。
第一,大脑对于事情的计划方式是线性的、阻塞的、单线程的语义,但是回调表达异步流 程的方式是非线性的、非顺序的,这使得正确推导这样的代码难度很大。难于理解的代码 是坏代码,会导致坏 bug。
我们需要一种更同步、更顺序、更阻塞的的方式来表达异步,就像我们的大脑一样。
第二,也是更重要的一点,回调会受到控制反转的影响,因为回调暗中把控制权交给第三方来调用你代码中的 continuation。 这种控制转移导 致一系列麻烦的信任问题,比如回调被调用的次数是否会超出预期、调用回调过早(在追踪之前)、 调用回调过晚(或没有调用)、调用回调的次数太少或太多(就像你遇到过的问题!)、没有把所需的环境/参数成功传给你的回调函数、吞掉可能出现的错误或异常。
第3章 Promise
如果不了解Promise,建议先看阮一峰的Promise介绍:docs.qq.com/doc/DSXlHUE9FRkVPVUNW
Promise 用于异步操作,表示一个还未完成但是预期会完成的操作。
-
Promise 特点
a. Promise对象的状态不受外界影响,pending 进行中状态、fulfilled 成功状态、rejected 失败状态,只有异步操作的结果可以决定当前是哪一种状态,其他任何操作都无法改变这个状态
b. Promise的状态一旦改变,就不会再变,任何时候都可以得到这个结果,状态不可以逆,只能由 pending变成fulfilled或者由pending变成rejected
-
Promise 的用法
a.Promise构造函数接受一个函数作为参数,该函数的两个参数分别是resolve和reject。它们是两个函数,由 JavaScript 引擎提供,不用自己部署
const promise = new Promise(function(resolve, reject) { // ... some code if (/* 异步操作成功 */){ resolve(value); } else { reject(error); } });
b. resolve函数的作用是,将Promise对象的状态从“未完成”变为“成功”(即从 pending 变为 resolved),在异步操作成功时调用,并将异步操作的结果,作为参数传递出去;
c. reject函数的作用是,将Promise对象的状态从“未完成”变为“失败”(即从 pending 变为 rejected),在异步操作失败时调用,并将异步操作报出的错误,作为参数传递出去。
d. Promise 新建后就会立即执行。
let promise = new Promise(function(resolve, reject) { console.log('Promise'); resolve(); }); promise.then(function() { console.log('resolved.'); }); console.log('Hi!'); // Promise // Hi! // resolved
-
then
then方法是定义在原型对象Promise.prototype上的。它的作用是为 Promise 实例添加状态改变时的回调函数。
then方法返回的是一个新的Promise实例。因此可以采用链式写法,即then方法后面再调用另一个then方法。
getJSON("/posts.json").then(function(json) { return json.post; }).then(function(post) { // ... });
-
catch
Promise.prototype.catch()方法是.then(null, rejection)或.then(undefined, rejection)的别名,用于指定发生错误时的回调函数。
getJSON('/posts.json').then(function(posts) { // ... }).catch(function(error) { // 处理 getJSON 和 前一个回调函数运行时发生的错误 console.log('发生错误!', error); });
上面代码中,getJSON()方法返回一个 Promise 对象,如果该对象状态变为resolved,则会调用then()方法指定的回调函数;如果异步操作抛出错误,状态就会变为rejected,就会调用catch()方法指定的回调函数,处理这个错误。
Promise 对象的错误具有“冒泡”性质,会一直向后传递,直到被捕获为止。也就是说,错误总是会被下一个catch语句捕获。
getJSON('/post/1.json').then(function(post) { return getJSON(post.commentURL); }).then(function(comments) { // some code }).catch(function(error) { // 处理前面三个Promise产生的错误 });
如果没有使用catch()方法指定错误处理的回调函数,Promise 对象抛出的错误不会传递到外层代码,即不会有任何反应。Promise 内部的错误不会影响到 Promise 外部的代码,通俗的说法就是“Promise 会吃掉错误”。
const someAsyncThing = function() { return new Promise(function(resolve, reject) { // 下面一行会报错,因为x没有声明 resolve(x + 2); }); }; someAsyncThing().then(function() { console.log('everything is great'); }); setTimeout(() => { console.log(123) }, 2000); // Uncaught (in promise) ReferenceError: x is not defined // 123
-
finally
finally()方法用于指定不管 Promise 对象最后状态如何,都会执行的操作。该方法是 ES2018 引入标准的。
promise .then(result => {···}) .catch(error => {···}) .finally(() => {···});
-
all
Promise.all()方法用于将多个 Promise 实例,包装成一个新的 Promise 实例。
const p = Promise.all([p1, p2, p3]);
p的状态由p1、p2、p3决定,分成两种情况。当p1、p2、p3的状态都变成fulfilled时,p的状态才变成fulfiled,当p1、p2、p3之中任一个被rejected,这的状态就变成rejected
-
race
Promise.race()方法同样是将多个 Promise 实例,包装成一个新的 Promise 实例。只要p1、p2、p3之中有一个实例率先改变状态,p的状态就跟着改变。那个率先改变的 Promise 实例的返回值,就传递给p的回调函数。
const p = Promise.race([p1, p2, p3]);
-
什么是 Promise
举例说明Promise概念
设想一下这样一个场景:我走到快餐店的柜台,点了一个芝士汉堡。我交给收银员1.47美元。通过下订单并付款,我已经发出了一个对某个值(就是那个汉堡)的请求。我已经启动了一次交易。但是,通常我不能马上就得到这个汉堡。收银员会交给我某个东西来代替汉堡:一张带有订单号的收据。订单号就是一个IOU(I owe you,我欠你的)承诺(promise),保证了最终我会得到我的汉堡。
我已经在想着未来的芝士汉堡了,尽管现在我还没有拿到手。我的大脑之所以可以这么做,是因为它已经把订单号当作芝士汉堡的占位符了。从本质上讲,这个占位符使得这个值不再依赖时间。这是一个未来值。
终于,我听到服务员在喊“订单113”,然后愉快地拿着收据走到柜台,把收据交给收银员,换来了我的芝士汉堡。
每次点芝士汉堡,我都知道最终要么得到一个芝士汉堡,要么得到一个汉堡包售罄的坏消息,那我就得找点别的当午饭了。假定执行一个任务,那么这个函数可能是立即完成也可能是需要一段时间才能完成。
我们需要知道的是这个函数什么时候结束,这样我们就可以进行下一个任务了。
因此我们需要实现监听,而监听执行结果的函数又两个,就是resolve和reject,这两个都是回调函数,resolve用于监听成功的结果(成功后执行),而reject则是失败,这就是resolve和reject的由来(知其所以然) -
信任问题
调用过早:根据定义,Promise就不必担心这种问题,因为即使是立即完成的Promise(类似于new Promise(function(resolve){ resolve(42); }))也无法被同步观察到。
调用过晚:一个Promise决议后,这个Promise上所有的通过then(..)注册的回调都会在下一个异步时机点上依次被立即调用。这些回调中的任意一个都无法影响或延误对其他回调的调用。
p.then( function(){ p.then( function(){ console.log( "C" ); } ); console.log( "A" ); } ); p.then( function(){ console.log( "B" ); } ); // A B C
回调未调用:如果你对一个Promise注册了一个完成回调和一个拒绝回调,那么Promise在决议时总是会调用其中的一个。如果Promise本身永远不被决议,可以用使Promise超时的工具timeoutPromise
调用次数过多或过少:如果出于某种原因,Promise创建代码试图调用resolve(..)或reject(..)多次,或者试图两者都调用,那么这个Promise将只会接受第一次决议,并默默地忽略任何后续调用。
未能传递参数/环境值:Promise至多只能有一个决议值(完成或拒绝)。如果你没有用任何值显式决议,那么这个值就是undefined。如果要传递多个值,你就必须要把它们封装在单个值中传递,比如通过一个数组或对象。
吞掉错误或异常:如果在Promise出现了一个JavaScript异常错误,比如一个TypeError或ReferenceError,那这个异常就会被捕捉,并且会使这个Promise被拒绝。
-
链式流
我们可以把多个 Promise 连接到一起表示一系列异步步骤。Promise 规范了异步,并封装了时间相关值的状态,因此我们可以把它们链式连接起来
调用Promise的then(..)会自动创建一个新的Promise从调用返回。
在完成或拒绝处理函数内部,如果返回一个值或抛出一个异常,新返回的(可链接的)Promise就相应地决议。
如果完成或拒绝处理函数返回一个Promise,它将会被展开,这样一来,不管它的决议值是什么,都会成为当前then(..)返回的链接Promise的决议值。
-
错误处理
一些开发者宣称Promise链的“最佳实践”是,总是将你的链条以catch(..)终结,就像这样:
var p = Promise.resolve( 42 ); p.then( function fulfilled(msg){ // 数字没有字符串方法, // 所以这里抛出一个错误 console.log( msg.toLowerCase() ); } ) .catch( handleErrors );
要是
handleErrors(..)
本身也有错误呢?谁来捕获它?你不能仅仅将另一个
catch(..)
贴在链条末尾,因为它也可能失败。Promise链的最后一步,无论它是什么,总有可能,即便这种可能性逐渐减少,悬挂着一个困在未被监听的Promise中的,未被捕获的错误。 -
Promise 局限性
Promise的设计局限性(具体来说,就是它们链接的方式)造成了一个让人很容易中招的陷阱,即Promise链中的错误很容易被无意中默默忽略掉。
Promise 只能有一个单一值或一个拒绝理由
Promise 只能决议一次(完成或拒绝)
一旦创建了一个Promise并为其注册了完成和/或拒绝处理函数,如果出现某种情况使得这个任务悬而未决的话,你也没有办法从外部停止它的进程。
Promise 进行的动作要多一些,自然意味着它会稍慢一些
第4章 生成器
如果不熟悉生成器,推荐先了解下生成器的教程:https://es6.ruanyifeng.com/#docs/generator
- 什么是生成器
- 普通函数与生成器函数的区别
- 生成器实现原理
- async/await 实现原理
第5章 性能
在这一章里,介绍了几种能够进一步提高性能的程序级别的机制。
- Worker
- SIMD
- asm.js
第6章 性能测试与调优
- Benchmark.js
- jsPerf.com
ES6与未来
起步上路
-
polyfilling和transpilling
polyfilling表示根据新特性的定义,创建一段与之行为等价但能够在旧的JavaScript环境中运行的代码。举例来说,ES6定义了一个名为Number.isNaN(..)的工具,用于提供一个精确无bug的NaN值检查,取代原来的isNaN(..)。但对这个工具进行兼容处理很容易,这样一来,无论终端用户是否使用ES6浏览器,你都能够开始使用它。
通过工具将新版代码转换为等价的旧版代码。这个过程通常被称为“transpiling”。它是由transforming(转换)和compiling(编译)组合而成的术语,如Babel
ES6及更新版本
第1章 ES?现在与未来
-
transpiling
通过工具将新版代码转换为等价的旧版代码。这个过程通常被称为“transpiling”。其思路是利用专门的工具把你的ES6代码转化为等价(或近似!)的可以在ES5环境下工作的代码。它是由transforming(转换)和compiling(编译)组合而成的术语,如Babel
// 转换前 var foo = [1,2,3]; var obj = { foo // 意思是 `foo: foo` }; obj.foo; // [1,2,3]
// 转换后 var foo = [1,2,3]; var obj = { foo: foo }; obj.foo; // [1,2,3]
-
polyfilling
polyfilling表示根据新特性的定义,创建一段与之行为等价但能够在旧的JavaScript环境中运行的代码。举例来说,ES6定义了一个名为Number.isNaN(..)的工具,用于提供一个精确无bug的NaN值检查,取代原来的isNaN(..)。但对这个工具进行兼容处理很容易,这样一来,无论终端用户是否使用ES6浏览器,你都能够开始使用它。
-
为什么oc上没有类似Babel的工具
更主要的 OC 本身已经是一款很成熟的语言了,它的语法变更已经很少很少,不像 ES 这么迭代频率还很高,做类似工具性价比低,现在更多的是运用在 JS <-> OC,Swift <-> OC 等这类不同语言互转上面;
第2章 语法
-
spread/rest
ES6引入了一个新的运算符...,通常称为spread或rest(展开或收集)运算符,取决于它在哪/如何使用
//展开 function foo(x,y,z) { console.log( x, y, z ); } foo( ...[1,2,3] ); // 1 2 3
//扩展 var a = [2,3,4]; var b = [ 1, ...a, 5 ]; console.log( b ); // [1,2,3,4,5]
-
解构
ES6 允许按照一定模式,从数组和对象中提取值,对变量进行赋值,这被称为解构(Destructuring)。
参考资料:https://es6.ruanyifeng.com/#docs/destructuring
下面代码表示,可以从数组中提取值,按照对应位置,对变量赋值。
let [a, b, c] = [1, 2, 3];
如果解构不成功,变量的值就等于undefined。
let [foo, [[bar], baz]] = [1, [[2], 3]]; foo // 1 bar // 2 baz // 3 let [ , , third] = ["foo", "bar", "baz"]; third // "baz" let [x, , y] = [1, 2, 3]; x // 1 y // 3 let [head, ...tail] = [1, 2, 3, 4]; head // 1 tail // [2, 3, 4] let [x, y, ...z] = ['a']; x // "a" y // undefined z // []
解构赋值允许指定默认值。注意,ES6 内部使用严格相等运算符(===),判断一个位置是否有值。所以,只有当一个数组成员严格等于undefined,默认值才会生效。
let [foo = true] = []; foo // true let [x, y = 'b'] = ['a']; // x='a', y='b' let [x, y = 'b'] = ['a', undefined]; // x='a', y='b' let [x = 1] = [undefined]; x // 1 let [x = 1] = [null]; x // null
如果默认值是一个表达式,那么这个表达式是惰性求值的,即只有在用到的时候,才会求值。
解构不仅可以用于数组,还可以用于对象。对象的解构与数组有一个重要的不同。数组的元素是按次序排列的,变量的取值由它的位置决定;而对象的属性没有次序,变量必须与属性同名,才能取到正确的值。
let { bar, foo } = { foo: 'aaa', bar: 'bbb' }; foo // "aaa" bar // "bbb" let { baz } = { foo: 'aaa', bar: 'bbb' }; baz // undefined
如果变量名与属性名不一致,必须写成下面这样。下面代码中,foo是匹配的模式,baz才是变量。真正被赋值的是变量baz,而不是模式foo。
let { foo: baz } = { foo: 'aaa', bar: 'bbb' }; baz // "aaa" let obj = { first: 'hello', last: 'world' }; let { first: f, last: l } = obj; f // 'hello' l // 'world'
-
解构的用途
(1)交换变量的值
let x = 1; let y = 2; [x, y] = [y, x];
(2)从函数返回多个值。函数只能返回一个值,如果要返回多个值,只能将它们放在数组或对象里返回。有了解构赋值,取出这些值就非常方便。
// 返回一个数组 function example() { return [1, 2, 3]; } let [a, b, c] = example(); // 返回一个对象 function example() { return { foo: 1, bar: 2 }; } let { foo, bar } = example();
(3)函数参数的定义,解构赋值可以方便地将一组参数与变量名对应起来。
// 参数是一组有次序的值 function f([x, y, z]) { ... } f([1, 2, 3]); // 参数是一组无次序的值 function f({x, y, z}) { ... } f({z: 3, y: 2, x: 1});
(4)提取 JSON 数据
let jsonData = { id: 42, status: "OK", data: [867, 5309] }; let { id, status, data: nub } = jsonData; console.log(id, status, nub); // 42, "OK", [867, 5309]
(5)函数参数的默认值
jQuery.ajax = function (url, { async = true, beforeSend = function () {}, cache = true, complete = function () {}, crossDomain = false, global = true, // ... more config } = {}) { // ... do stuff };
(6)遍历 Map 结构
const map = new Map(); map.set('first', 'hello'); map.set('second', 'world'); for (let [key, value] of map) { console.log(key + " is " + value); } // first is hello // second is world
如果只想获取键名,或者只想获取键值,可以写成下面这样。
// 获取键名 for (let [key] of map) { // ... } // 获取键值 for (let [,value] of map) { // ... }
(7)输入模块的指定方法。加载模块时,往往需要指定输入哪些方法
const { SourceMapConsumer, SourceNode } = require("source-map");
-
插入字符串字面量
在一组字符外用
..
来包围,这会被解释为一个字符串字面量,但是其中任何${..}形式的表达式都会被立即在线解析求值。这种形式的解析求值形式就是插入(比模板要精确一些)。var name = "Kyle"; var greeting = `Hello ${name}!`; console.log( greeting ); // "Hello Kyle!" console.log( typeof greeting ); // "string"
插入字符串字面量的一个优点是它们可以分散在多行
var text = `Now is the time for all good men to come to the aid of their country!`; console.log( text ); // Now is the time for all good men // to come to the aid of their // country!
插入字符串字面量的${..}内可以出现任何合法的表达式,包括函数调用、在线函数表达式调用,甚至其他插入字符串字面量!
function upper(s) { return s.toUpperCase(); } var who = "reader"; var text = `A very ${upper( "warm" )} welcome to all of you ${upper( `${who}s` )}!`; console.log( text ); // A very WARM welcome // to all of you READERS!
ES6提供了一个内建函数可以用作字符串字面量标签:String.raw(..)。它就是传出strings的原始版本:
console.log( `Hello\nWorld` ); // Hello // World console.log( String.raw`Hello\nWorld` ); // Hello\nWorld String.raw`Hello\nWorld`.length; // 12
-
箭头函数
=>箭头函数的主要设计目的就是以特定的方式改变this的行为特性,解决this相关编码的一个特殊而又常见的痛点。节省的输入字符不是主要目的。
在箭头函数内部,this绑定不是动态的,而是词法的。在前面的代码中,如果使用箭头函数作为回调,this则如我们所愿是可预测的。
var controller = { makeRequest: function(..){ btn.addEventListener( "click", () => { // .. this.makeRequest(..); }, false ); } };
-
for..of循环
对比一下for..of和for..in以展示其中的区别:可以看到,for..in在数组a的键/索引上循环,而for..of在a的值上循环。
var a = ["a","b","c","d","e"]; for (var idx in a) { console.log( idx ); } // 0 1 2 3 4 for (var val of a) { console.log( val ); } // "a" "b" "c" "d" "e"
-
symbol
symbol。与其他的基本类型不同,symbol没有字面形式。
var sym = Symbol( "some optional description" ); typeof sym; // "symbol" sym.toString(); // "Symbol(some optional description)" sym instanceof Symbol; // false var symObj = Object( sym ); symObj instanceof Symbol; // true symObj.valueOf() === sym; // true
符号的主要意义是创建一个类(似)字符串的不会与其他任何值冲突的值。
考虑使用一个符号作为事件名的常量表示的例子,这里的好处是EVT LOGIN持有一个不可能与其他值(有意或无意)重复的值,所以这里分发或处理的事件不会有任何混淆。
const EVT_LOGIN = Symbol( "event.login" ); evthub.listen( EVT_LOGIN, function(data){ // .. } );
考虑这个实现了 单例 模式行为的模块 —— 也就是,它仅允许自己被创建一次:这里的INSTANCE符号值是一个特殊的、几乎隐藏的、类似元属性的属性,静态保存在HappeyFace()函数对象中。
const INSTANCE = Symbol( "instance" ); function HappyFace() { if (HappyFace[INSTANCE]) return HappyFace[INSTANCE]; function smile() { .. } return HappyFace[INSTANCE] = { smile: smile }; } var me = HappyFace(), you = HappyFace(); me === you; // true
上面的只能在自身作用域内使用,通过全局符号注册(global symbol registry)创建这些符号值,就可以在全局作用域访问
const EVT_LOGIN = Symbol.for( "event.login" ); console.log( EVT_LOGIN ); // Symbol(event.login) function HappyFace() { const INSTANCE = Symbol.for( "instance" ); if (HappyFace[INSTANCE]) return HappyFace[INSTANCE]; // .. return HappyFace[INSTANCE] = { .. }; }
Symbol.for(..)在全局符号注册表中搜索,来查看是否有描述文字相同的符号已经存在,如果有的话就返回它。如果没有的话,会新建一个并将其返回。换句话说,全局注册表把符号值本身根据其描述文字作为单例处理。
可以使用Symbol.keyFor(..)提取注册符号的描述文本(键值):
var s = Symbol.for( "something cool" ); var desc = Symbol.keyFor( s ); console.log( desc ); // "something cool" // 再次从注册表取得symbol var s2 = Symbol.for( desc ); s2 === s; // true
如果把符号用作对象的属性/键值,那么它会以一种特殊的方式存储,使得这个属性不出现在对这个对象的一般属性枚举中:
var o = { foo: 42, [ Symbol( "bar" ) ]: "hello world", baz: true }; Object.getOwnPropertyNames( o ); // [ "foo","baz" ] Object.getOwnPropertySymbols( o ); // [ Symbol(bar) ]
第3章 代码组织
-
迭代器
参考资料:https://es6.ruanyifeng.com/#docs/iterator
迭代器(iterator)是一种有序的、连续的、基于拉取的用于消耗数据的组织方式,用于从源以一次一个的方式提取数据。例如,你可以实现一个工具,在每次请求的时候产生一个新的唯一标识符。也可以在一个固定列表上以轮询的方式产生一个无限值序列。或者也可以把迭代器附着在一个数据库查询结果上,每次迭代拉出一个新行。
Iterator 的作用有三个:一是为各种数据结构,提供一个统一的、简便的访问接口;二是使得数据结构的成员能够按某种次序排列;三是 ES6 创造了一种新的遍历命令for...of循环,Iterator 接口主要供for...of消费。
Iterator 的遍历过程是这样的。
(1)创建一个指针对象,指向当前数据结构的起始位置。也就是说,遍历器对象本质上,就是一个指针对象。
(2)第一次调用指针对象的
next
方法,可以将指针指向数据结构的第一个成员。(3)第二次调用指针对象的
next
方法,指针就指向数据结构的第二个成员。(4)不断调用指针对象的
next
方法,直到它指向数据结构的结束位置。每一次调用next方法,都会返回数据结构的当前成员的信息。具体来说,就是返回一个包含value和done两个属性的对象。其中,value属性是当前成员的值,done属性是一个布尔值,表示遍历是否结束。
-
ES6模块的特性
ES6使用基于文件的模块,也就是说一个文件一个模块。目前,还没有把多个模块合并到单个文件中的标准方法。
ES6模块的API是静态的。也就是说,需要在模块的公开API中静态定义所有最高层导出,之后无法补充。
ES6模块是单例。也就是说,模块只有一个实例,其中维护了它的状态。每次向其他模块导入这个模块的时候,得到的是对单个中心实例的引用。如果需要产生多个模块实例,那么你的模块需要提供某种工厂方法来实现这一点。
模块的公开API中暴露的属性和方法并不仅仅是普通的值或引用的赋值。它们是到内部模块定义中的标识符的实际绑定(几乎类似于指针)。如果导出一个局部私有变量,实际导出的都是到这个变量的绑定。如果模块修改了这个变量的值,外部导入绑定现在会决议到新的值。
-
export
export function foo() { // .. } export var awesome = 42; var bar = [1,2,3]; export { bar };
没有用export标示的一切都在模块作用域内部保持私有。
在命名导出期间“重命名”(也叫别名)一个模块成员
function foo() { .. } export { foo as bar };
导入这个模块的时候,不管是在awesome = 100之前还是之后,一旦赋值发生,导入的绑定就会决议到100而不是42。因为绑定是一个指向awesome变量本身的引用或者指针,而不是这个值的复制
var awesome = 42; export { awesome }; // 稍后 awesome = 100;
推荐使用默认导出,这样有更简单的import语法。但每个模块定义只能有一个default。
-
import
如果这个模块只有一个你想要导入并绑定到一个标识符的默认导出,绑定时可以省略包围的{ .. }语法。
import foo from "foo"
export default function foo() { .. } export function bar() { .. } export function baz() { .. }
导入这个模块的默认导出和它的两个命名导出:
import FOOFN, { bar, baz as BAZ } from "foo"; FOOFN(); bar(); BAZ();
理想的选择是从模块把所有一切导入到一个单独命名空间,而不是向作用域直接导入独立的成员。幸运的是,import语句有一种语法变体可以支持这种模块导入,称为命名空间导入(namespace import)。
你可以把整个API导入到单个模块命名空间绑定:
export function bar() { .. } export var x = 42; export function baz() { .. } import * as foo from "foo"; foo.bar(); foo.x; // 42 foo.baz();
-
模块依赖环
A导入B, B导入A,这样是可行的。
本质上说,相互导入,加上检验两个import语句的有效性的静态验证,虚拟组合了两个独立的模块空间(通过绑定),这样foo(..)可以调用bar(..),反过来也是一样。这和如果它们本来是声明在同一个作用域中是对称的。
第5章 集合
-
TypedArray
ArrayBuffer对象、TypedArray视图和DataView视图是 JavaScript 操作二进制数据的一个接口.
(1)
ArrayBuffer
对象:代表内存之中的一段二进制数据,可以通过“视图”进行操作。“视图”部署了数组接口,这意味着,可以用数组的方法操作内存。(2)
TypedArray
视图:共包括 9 种类型的视图,比如Uint8Array
(无符号 8 位整数)数组视图,Int16Array
(16 位整数)数组视图,Float32Array
(32 位浮点数)数组视图等等。(3)
DataView
视图:可以自定义复合格式的视图,比如第一个字节是 Uint8(无符号 8 位整数)、第二、三个字节是 Int16(16 位整数)、第四个字节开始是 Float32(32 位浮点数)等等,此外还可以自定义字节序。简单说,
ArrayBuffer
对象代表原始的二进制数据,TypedArray
视图用来读写简单类型的二进制数据,DataView
视图用来读写复杂类型的二进制数据。 -
Map
JavaScript 的对象(Object),本质上是键值对的集合(Hash 结构),但是传统上只能用字符串当作键。这给它的使用带来了很大的限制。Object 结构提供了“字符串—值”的对应,Map 结构提供了“值—值”的对应,是一种更完善的 Hash 结构实现。如果你需要“键值对”的数据结构,Map 比 Object 更合适。
const m = new Map(); const o = {p: 'Hello World'}; m.set(o, 'content') m.get(o) // "content" m.has(o) // true m.delete(o) // true m.has(o) // false
Map的基本操作,增删改查
let m = new Map() //增,key可以是任意值,不仅仅是字符串 m.set('a', 'aaa') m.set(['b'], 'bbb') //删,delete方法删除某个键,返回true。如果删除失败,返回false。clear方法清除所有 m.delete(['b']) //改,如果key已经有值,则键值会被更新,否则就新生成该键。 m.set('a', 'aaaaaaa') //查 m.has('a') // true m.get('a') // aaaaaaa m.size // 1
Map的遍历,提供了三个遍历器生成函数和一个遍历方法
-
Map.prototype.keys()
:返回键名的遍历器。 -
Map.prototype.values()
:返回键值的遍历器。 -
Map.prototype.entries()
:返回所有成员的遍历器。 -
Map.prototype.forEach()
:遍历 Map 的所有成员。
需要特别注意的是,Map 的遍历顺序就是插入顺序。
Map与其他数据结构的互相转换
(1)Map 转为数组
const myMap = new Map() .set(true, 7) .set({foo: 3}, ['abc']); [...myMap] // [ [ true, 7 ], [ { foo: 3 }, [ 'abc' ] ] ]
(3)Map 转为对象
如果有非字符串的键名,那么这个键名会被转成字符串,再作为对象的键名。
function strMapToObj(strMap) { let obj = Object.create(null); for (let [k,v] of strMap) { obj[k] = v; } return obj; } const myMap = new Map() .set('yes', true) .set('no', false); strMapToObj(myMap) // { yes: true, no: false }
(4)对象转为 Map
对象转为 Map 可以通过Object.entries()。
let obj = {"a":1, "b":2}; let map = new Map(Object.entries(obj));
-
-
WeakMap
WeakMap与Map的区别有两点。
首先,WeakMap只接受对象作为键名(null除外),不接受其他类型的值作为键名。
其次,WeakMap的键名所指向的对象,不计入垃圾回收机制。
WeakMap 就是为了解决这个问题而诞生的,它的键名所引用的对象都是弱引用,即垃圾回收机制不将该引用考虑在内。因此,只要所引用的对象的其他引用都被清除,垃圾回收机制就会释放该对象所占用的内存。也就是说,一旦不再需要,WeakMap 里面的键名对象和所对应的键值对会自动消失,不用手动删除引用。
-
Set
ES6 提供了新的数据结构 Set。它类似于数组,但是成员的值都是唯一的,没有重复的值。
Set 实例的属性和方法
Set 结构的实例有以下属性。
-
Set.prototype.constructor
:构造函数,默认就是Set
函数。 -
Set.prototype.size
:返回Set
实例的成员总数。
Set 实例的方法分为两大类:操作方法(用于操作数据)和遍历方法(用于遍历成员)。下面先介绍四个操作方法。
-
Set.prototype.add(value)
:添加某个值,返回 Set 结构本身。 -
Set.prototype.delete(value)
:删除某个值,返回一个布尔值,表示删除是否成功。 -
Set.prototype.has(value)
:返回一个布尔值,表示该值是否为Set
的成员。 -
Set.prototype.clear()
:清除所有成员,没有返回值。
Set 结构的实例有四个遍历方法,可以用于遍历成员。
-
Set.prototype.keys()
:返回键名的遍历器 -
Set.prototype.values()
:返回键值的遍历器 -
Set.prototype.entries()
:返回键值对的遍历器 -
Set.prototype.forEach()
:使用回调函数遍历每个成员
由于 Set 结构没有键名,只有键值(或者说键名和键值是同一个值),所以keys方法和values方法的行为完全一致。可以省略values方法,直接用for...of循环遍历 Set。
Set的应用
扩展运算符(...)内部使用for...of循环,所以也可以用于 Set 结构。
let set = new Set(['red', 'green', 'blue']); let arr = [...set]; // ['red', 'green', 'blue']
扩展运算符和 Set 结构相结合,就可以去除数组的重复成员。
let arr = [3, 5, 2, 2, 5, 5]; let unique = [...new Set(arr)]; // [3, 5, 2]
而且,数组的map和filter方法也可以间接用于 Set 了。
let set = new Set([1, 2, 3]); set = new Set([...set].map(x => x * 2)); // 返回Set结构:{2, 4, 6} let set = new Set([1, 2, 3, 4, 5]); set = new Set([...set].filter(x => (x % 2) == 0)); // 返回Set结构:{2, 4}
因此使用 Set 可以很容易地实现并集(Union)、交集(Intersect)和差集(Difference)。
let a = new Set([1, 2, 3]); let b = new Set([4, 3, 2]); // 并集 let union = new Set([...a, ...b]); // Set {1, 2, 3, 4} // 交集 let intersect = new Set([...a].filter(x => b.has(x))); // set {2, 3} // (a 相对于 b 的)差集 let difference = new Set([...a].filter(x => !b.has(x))); // Set {1}
提供了两种方法,直接在遍历操作中改变原来的 Set 结构。
// 方法一 let set = new Set([1, 2, 3]); set = new Set([...set].map(val => val * 2)); // set的值是2, 4, 6 // 方法二 let set = new Set([1, 2, 3]); set = new Set(Array.from(set, val => val * 2)); // set的值是2, 4, 6
-
-
WeakSet
Set与WeakSet的区别,首先,WeakSet 的成员只能是对象,而不能是其他类型的值。
其次,WeakSet 中的对象都是弱引用,即垃圾回收机制不考虑 WeakSet 对该对象的引用,也就是说,如果其他对象都不再引用该对象,那么垃圾回收机制会自动回收该对象所占用的内存,不考虑该对象还存在于 WeakSet 之中。
第6章 新增API
-
Array与扩展运算符
扩展运算符(spread)是三个点(...)。它好比 rest 参数的逆运算,将一个数组转为用逗号分隔的参数序列。
console.log(...[1, 2, 3]) // 1 2 3 console.log(1, ...[2, 3, 4], 5) // 1 2 3 4 5 [...document.querySelectorAll('div')] // [<div>, <div>, <div>]
扩展运算符的应用
(1)复制数组
const a1 = [1, 2]; // 写法一 const a2 = [...a1]; // 写法二 const [...a2] = a1;
(2)合并数组
这两种方法都是浅拷贝,使用的时候需要注意。新数组的成员都是对原数组成员的引用,这就是浅拷贝。如果修改了引用指向的值,会同步反映到新数组。
const arr1 = ['a', 'b']; const arr2 = ['c']; const arr3 = ['d', 'e']; // ES5 的合并数组 arr1.concat(arr2, arr3); // [ 'a', 'b', 'c', 'd', 'e' ] // ES6 的合并数组 [...arr1, ...arr2, ...arr3] // [ 'a', 'b', 'c', 'd', 'e' ]
(3)与解构赋值结合
const [first, ...rest] = [1, 2, 3, 4, 5]; first // 1 rest // [2, 3, 4, 5] const [first, ...rest] = []; first // undefined rest // [] const [first, ...rest] = ["foo"]; first // "foo" rest // []
-
Array
Array.from()方法用于将两类对象转为真正的数组:类似数组的对象(array-like object)和可遍历(iterable)的对象(包括 ES6 新增的数据结构 Set 和 Map)
let arrayLike = { '0': 'a', '1': 'b', '2': 'c', length: 3 }; let arr2 = Array.from(arrayLike); // ['a', 'b', 'c'] Array.from('hello') // ['h', 'e', 'l', 'l', 'o'] let namesSet = new Set(['a', 'b']) Array.from(namesSet) // ['a', 'b']
Array.from还可以接受第二个参数,作用类似于数组的map方法,用来对每个元素进行处理,将处理后的值放入返回的数组。
Array.from(arrayLike, x => x * x); // 等同于 Array.from(arrayLike).map(x => x * x); Array.from([1, 2, 3], (x) => x * x) // [1, 4, 9] Array.from({ length: 2 }, () => 'jack') // ['jack', 'jack']
Array.of方法用于将一组值,转换为数组。Array.of基本上可以用来替代Array()或new Array(),并且不存在由于参数不同而导致的重载。它的行为非常统一。
Array.of() // [] Array.of(undefined) // [undefined] Array.of(1) // [1] Array.of(1, 2) // [1, 2]
fill方法使用给定值,填充一个数组。fill方法还可以接受第二个和第三个参数,用于指定填充的起始位置和结束位置。
['a', 'b', 'c'].fill(7) // [7, 7, 7] new Array(3).fill(7) // [7, 7, 7] ['a', 'b', 'c'].fill(7, 1, 2) // ['a', 7, 'c']
Array.prototype.includes方法返回一个布尔值,表示某个数组是否包含给定的值,与字符串的includes方法类似。ES2016 引入了该方法。
[1, 2, 3].includes(2) // true [1, 2, 3].includes(4) // false [1, 2, NaN].includes(NaN) // true
Array.prototype.flat()用于将嵌套的数组“拉平”,变成一维的数组。该方法返回一个新数组,对原数据没有影响。
[1, 2, [3, 4]].flat() // [1, 2, 3, 4] [1, 2, [3, [4, 5]]].flat() // [1, 2, 3, [4, 5]] //可以将flat()方法的参数写成一个整数,表示想要拉平的层数,默认为1。 [1, 2, [3, [4, 5]]].flat(2) // [1, 2, 3, 4, 5]
如果不管有多少层嵌套,都要转成一维数组,可以用Infinity关键字作为参数。
[1, [2, [3]]].flat(Infinity) // [1, 2, 3]
flatMap()方法对原数组的每个成员执行一个函数(相当于执行Array.prototype.map()),然后对返回值组成的数组执行flat()方法。该方法返回一个新数组,不改变原数组。flatMap()只能展开一层数组。
// 相当于 [[2, 4], [3, 6], [4, 8]].flat() [2, 3, 4].flatMap((x) => [x, x * 2]) // [2, 4, 3, 6, 4, 8]
数组的空位指,数组的某一个位置没有任何值。比如,Array构造函数返回的数组都是空位。ES6 则是明确将空位转为undefined。由于空位的处理规则非常不统一,所以建议避免出现空位。
-
Object
Object.is就是部署这个算法的新方法。它用来比较两个值是否严格相等,与严格比较运算符(===)的行为基本一致。Object.is就是部署这个算法的新方法。它用来比较两个值是否严格相等,与严格比较运算符(===)的行为基本一致。
+0 === -0 //true NaN === NaN // false Object.is(+0, -0) // false Object.is(NaN, NaN) // true
Object.assign()方法用于对象的合并,将源对象(source)的所有可枚举属性,复制到目标对象(target)。
注意,如果目标对象与源对象有同名属性,或多个源对象有同名属性,则后面的属性会覆盖前面的属性。
const target = { a: 1 }; const source1 = { b: 2 }; const source2 = { c: 3 }; Object.assign(target, source1, source2); target // {a:1, b:2, c:3}
Object.assign()方法实行的是浅拷贝,而不是深拷贝。也就是说,如果源对象某个属性的值是对象,那么目标对象拷贝得到的是这个对象的引用。
const obj1 = {a: {b: 1}}; const obj2 = Object.assign({}, obj1); obj1.a.b = 2; obj2.a.b // 2
assisgn方法可以用来为属性指定默认值
const DEFAULTS = { logLevel: 0, outputFormat: 'html' }; function processContent(options) { options = Object.assign({}, DEFAULTS, options); console.log(options); // ... }
-
Number
ES6 在Number对象上,新提供了Number.isFinite()和Number.isNaN()两个方法。Number.isFinite()用来检查一个数值是否为有限的(finite),即不是Infinity。Number.isNaN()用来检查一个值是否为NaN。
Number.isFinite(15); // true Number.isFinite(0.8); // true Number.isFinite(NaN); // false Number.isFinite(Infinity); // false Number.isFinite(-Infinity); // false Number.isFinite('foo'); // false Number.isFinite('15'); // false Number.isFinite(true); // false Number.isNaN(NaN) // true Number.isNaN(15) // false Number.isNaN('15') // false Number.isNaN(true) // false Number.isNaN(9/NaN) // true Number.isNaN('true' / 0) // true Number.isNaN('true' / 'true') // true
ES6 将全局方法parseInt()和parseFloat(),移植到Number对象上面,行为完全保持不变。
Number.parseInt === parseInt // true Number.parseFloat === parseFloat // true
Number.isInteger()用来判断一个数值是否为整数。
Number.isInteger(25) // true Number.isInteger(25.1) // false Number.isInteger(25.0) // true Number.isInteger() // false Number.isInteger(null) // false Number.isInteger('15') // false Number.isInteger(true) // false
第7章 元编程
-
Proxy
Proxy 用于修改某些操作的默认行为,等同于在语言层面做出修改,所以属于一种“元编程”(meta programming),即对编程语言进行编程。
Proxy 可以理解成,在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写。Proxy 这个词的原意是代理,用在这里表示由它来“代理”某些操作,可以译为“代理器”。
使用说明请参考资料 https://es6.ruanyifeng.com/#docs/proxy
-
Reflect
使用说明请参考资料 https://es6.ruanyifeng.com/#docs/reflect
网友评论