第一章 块级作用域绑定
let
和 const
都是不存在提升,声明的都是块级标识符
都禁止重声明
var a = 30;
var message = 2;
// 这两条都会抛出语法错误
let a = 40;
const message = 1;
每个const
声明的常量必须进行初始化
const
定义的常量不能修改,但是用const声明的对象可以修改值
const name; // 语法错误:常量未初始化
const person = {
name: 'a'
};
person.name = 'b'; // 可以修改
// SyntaxError: "person" is read-only
person = {
name: 'Jone'
}
临时死区(Temporal Dead Zone)
let
和const
声明不会像var
一样提升到作用域顶部,如果在声明之前访问这些变量,即使是相对安全的typeof
操作符也会触发引用错误。用let
来举例(const也一样)
if(1){ // 加不加if(1)结果一样
// Uncaught ReferenceError: value is not defined
console.log(typeof value);
let value = 1;
}
console.log(typeof value2); // "undefined"
if(1){
let value2 = 1;
}
由于console.log(typeof value)
语句会抛出错误,因此用let定义并初始化变量value的语句不会执行。此时的value还位于JavaScript社区所谓的临时死区TDZ中。
JavaScript引擎在扫描代码发现变量声明时,要么将它们提升至作用域顶部(var),要么把声明放到TDZ中(let、const)。访问TDZ中的变量会触发运行时错误。只有执行过变量声明语句后,变量才会从TDZ中移除,方可正常访问。
上述的第二个例子 typeof 是在声明变量value2的代码块外执行的,此时value2并不在TDZ中,这也就意味着不存在value2这个绑定,typeof返回undefined
循环中的let
没什么可说的,不用再使用IIFE了
循环中的const
for(const i=0; i<10; i++){ // Uncaught TypeError: Assignment to constant variable.
console.log(i)
}
var object = {a:1,b:1,c:1}; // 不会报错
for(const key in object){
console.log(key)
}
第一个 for循环必然会报错
第二个 不会报错的原因是for-in和for-of循环中,每次迭代不会像for循环一样修改已有的绑定,而是会创建一个新绑定
全局块作用域绑定
在全局作用域中,var 声明的变量会成为全局对象(浏览器环境中的window)的属性。这意味着var很可能会无意中覆盖一个已经存在的全局变量。
var Test=1;
window.Test === Test; // true
如果在全局作用域中使用let或const,会在全局作用域下创建一个新的绑定,但该绑定不会添加为全局对象的属性。换句话说,用let
或const
不能覆盖全局变量,而只能遮蔽它。
const foo = 1;
window.foo = 2;
console.log(foo); // 1
console.log(window.foo); // 2
块级绑定最佳实践的进化
ECMAScript6标准尚在开发中时,人们普遍认为应该默认使用let
而不是var
。对很多JavaScript开发者而言,let
实际上与他们想要的var
一样,直接替换符合逻辑。这种情况下,对于需要些保护的变量则要使用const。
然而,当更多开发者迁移到ECMAScript6后,另一种做法日益普及:默认使用const,只有确实需要改变变量的值时使用let
。因为大部分变量的值在初始化后不应再改变,而预料外的变量值的改变是很多bug的源头。
第二章 字符串和正则表达式
暂略
第三章 函数
1. 函数形参的默认值
JavaScript函数有一个特别的地方,无论在函数定义中声明了多少形参,都可以传入任意数量的参数,也可以在定义函数时添加针对参数数量的处理逻辑,当已定义的形参无对应的传入参数时为期指定一个默认值。
1.1 ECMAScript5中模拟默认参数
function makeRequest(url, timeout, callback){
timeout = timeout || 2000;
callback = callback || function(){}
}
这样做的一个缺陷是 如果想指定给timeout的值就是0,也会被赋值2000。所以更稳妥的办法是使用typeof
检查参数类型
function makeRequest(url, timeout, callback){
timeout = (typeof timeout !== 'undefined')? timeout : 2000;
callback = (typeof callback !== 'undefined')? callback : function(){}
}
在流行的JavaScript库中均使用类似的模式进行补全。
1.2 ECMAScript6中的默认参数值
function makeRequest(url, timeout=2000, callback=function(){}){
}
声明函数时,可以为任意参数指定默认值,在已指定默认值的参数后可以继续声明无默认值参数。如上例,callback可以无默认值。
1.3 默认参数值对arguments对象的影响
ES5非严格模式下,函数命名参数的变化会体现在arguments对象中
function mixArgs(first, second){
console.log(first === arguments[0]); //true
console.log(second === arguments[1]); //true
first = 'c';
second = 'd';
console.log(first === arguments[0]); //true
console.log(second === arguments[1]); //true
}
严格模式下,first和second的值不会导致arguments改变。
function mixArgs(first, second){
"use strict";
console.log(first === arguments[0]); //true
console.log(second === arguments[1]); //true
first = 'c';
second = 'd';
console.log(first === arguments[0]); //false
console.log(second === arguments[1]); //false
}
在ES6函数使用默认参数值时,无论是否显示定义了严格模式,arguments对象的行为都将与ES5严格模式下保持一致。默认参数值的存在使得arguments对象保持与命名参数分离。
function mixArgs(first, second = 'b'){
console.log(arguments.length);
console.log(first === arguments[0]);
console.log(second === arguments[1]);
first = 'c';
second = 'd';
console.log(first === arguments[0]);
console.log(second === arguments[1]);
}
mixArgs('a');
// 1 true false false false false
1.4 默认参数表达式
关于默认参数值,最有趣的挺特性可能是非原始值传参。
function getValue(){
return 5;
}
function add(first, second = getValue()){
return first + second;
}
console.log(add(1,1)); // 2
console.log(add(1)); // 6
这段代码中,如果不传入最后一个参数,就会调用getValue()函数来得到正确的默认值。切记初次解析函数生命时,不会调用getValue()方法,只有当调用add()函数且不传入第二个参数时才会调用
let value = 5;
function getValue(){
return value++;
}
function add(first, second = getValue()){
return first + second;
}
add(1,1); // 2
add(1); // 6
add(1); // 7
注意,当使用函数调用结果作为默认参数值时,如果忘记写小括号,例如,second=getValue, 则最终传入的是对函数的引用,而不是函数调用的结果。
正因为默认参数实在函数调用时求值,所以可以使用先定义的参数作为后定义参数的默认值(反过来不可以)
function add(first, second = first){
return first + second;
}
add(1, 1); // 2
add(1); // 2
1.5 默认参数的临时死区
第一章介绍let
和const
时我们介绍了临时死区TDZ,其实默认参数也有同样的临时死区,在这里的参数不可访问。与let
声明类似,定义参数时会为每个参数创建一个新的标识符绑定,该绑定在初始化之前不可被引用,如果试图访问会导致程序抛出错误。当调用函数时,会通过传入的值或参数的默认值初始化该参数。
function getValue(value){
return value + 5;
}
function add(first, second = getValue(first)){
return first + second;
}
add(1,1); // 2
add(1); // 7
调用add(1, 1)和add(1)时实际相当于执行以下代码来创建fist和second参数值:
// add(1,1)时执行的JavaScript代码
let first = 1;
let second = 1;
// add(1)时的JavaScript代码
let first = 1;
let second = getValue(first);
1.4节中提到过不可以 后定义的参数作为先定义参数的默认值。
function add(first=senond, second){
return first + second;
}
add(1,1) //2
// 执行的函数
// let first = 1;
// let second = 1;
add(undefined, 1) //抛出错误
// 执行的函数
// let first = second;
// let second = 1;
可见,调用add(undefined, 1)函数时,因为当first初始化时second尚未初始化,所以会导致程序抛出错误,此时second尚处于临时死区中。
函数参数有自己的作用域和临时死区,与其函数体的作用域是各自独立的,也就是说参数的默认值不可访问函数体内声明的变量。
2. 处理无命名参数
到目前为止,本章中的示例使用到的参数都是命名参数。然而JavaScript的函数语法规定,无论函数已定义的命名参数有多少毛豆不限制调用时传入的实际参数数量,调用时总是可以传入任意数量的参数。
2.1 ES5中的无命名参数
下面pick函数模仿了Underscore.js库中的pick()方法,返回一个给定对象的副本,包含原始对象属性的特定子集。
function pick(object) {
let result = Object.create(null);
for (let i = 1, len = arguments.length; i < len; i++) {
result[arguments[i]] = object[arguments[i]];
}
return result;
}
let book = {
title: "Understanding ECMAScrpt6",
author: "NowhereToRun",
year: "2017"
};
let bookData = pick(book, "author", "year");
console.log(bookData);
关于pick函数应该注意这样几件事情:
首先,并不容易发现这个函数可以接受任意数量的参数。
其次,因为第一参数为命名参数并且已被使用,当你要查找需要拷贝的属性名称时,不得不从索引1而不是索引0开始遍历arguments对象。
而在ES6中,通过不定参数(rest parameters)的特性可以解决这个问题。
2.2 不定参数
function pick(object, ...keys) {
let result = Object.create(null);
for (let i = 0, len = keys.length; i < len; i++) {
result[keys[i]] = object[keys[i]];
}
return result;
}
不定参数keys包含的是object知乎传入的所有参数(而arguments对象包含的则是所有传入的参数,包括object)这样一来你就可以放心地遍历keys对象了。这种方法还有一个好处,秩序看一眼函数就可以知晓该函数可以处理的参数数量。
函数的length属性统计的是函数命名参数的数量,不定参数的加入不会影响length属性的值。在本例中,pick函数的length值为1,因为只会计算object。(即与改写成为使用不定参数的函数之前一样)
2.3 不定参数的使用限制
- 每个函数最多只能声明一个不定参数,而且一定要放在所有参数的末尾
- 不定参数不能用于对象字面量setter之中
function pick(object, ...keys , last) {
// SyntaxError: Rest parameter must be last formal parameter
let result = Object.create(null);
for (let i = 0, len = keys.length; i < len; i++) {
result[keys[i]] = object[keys[i]];
}
return result;
}
let object = {
// SyntaxError: Setter function argument must not be a rest parameter
set name(...name){
// 执行一些逻辑
}
}
2.4 不定参数对arguments对象的影响
无论是否使用不定参数,arguments对象总是包含所有传入函数的参数。
3. 增强的Function构造函数
Function构造函数是JavaScript语法中很少被使用到的一部分,通常我们用它来动态创建新的函数。这种构造函数接受字符串形式的参数,分别为函数的参数及函数体。
var add = new Function("first", "second", "return first + second");
console.log(add(1, 1)); // 2
ES6中增强了Function构造函数的功能,支持在创建函数时定义默认参数和不定参数。唯一需要做的是在参数名后面添加一个等号及一个默认值。
var add = new Function("first", "second = first", "return first + second");
console.log(add(1, 1)); // 2
console.log(add(1)); // 2
定义不定参数,只需在最后一个参数前添加...
var pickFirst = new Function("...args","return args[0]");
console.log(pickFirst(1,2,3,4)); // 1
4. 展开运算符
简单粗暴,看个例子,不解释太多
let values = [25,50,75,100];
console.log(Math.max.apply(Math, values));
console.log(Math.max(...values));
可以将展开运算符与其他正常传入的参数混合使用。例如像限定返回的最小值为0.
let values = [-25, -50, -75, -100];
console.log(Math.max(...values, 0));
5. name属性
由于在JavaScript中有多种定义函数的方式,因而辨别函数就是一项具有挑战性的任务,而且匿名函数表达式的广泛使用更是加大了调试难度。于是ES6中为所有的函数新增了name属性。
function doSomething() { }
var doAnotherSomething = function () { };
console.log(doSomething.name); // doSomething
console.log(doAnotherSomething.name); // doAnotherSomething
var doSth = function doSthElse() { };
var person = {
get firstName() {
return "HeiHeiHei"
},
sayName: function () {
console.log(this.name);
}
}
console.log(doSth.name); // doSthElse
console.log(person.sayName.name); // sayName
console.log(person.firstName.name); // get firstName (undefined)
var bindName = function(){};
console.log(bindName.bind().name); // bound bindName
console.log((new Function).name); // anonymous
前两个没什么说的。
第三个 doSth,可见权重。(书上原话:函数表达式有一个名字,这个名字比函数本身被赋值的变量的权重高,感觉好像不是一个意思)
第五个firstName,目前Chrome(59.0.3)测试是undefined
第六个,绑定函数的name属性总是由被绑定函数的name属性及字符串前缀bound
组成
第七个,通过Function构造函数创建的函数,其名称将带有前缀anonymous
切记,函数name属性的值不一定引用同名变量,他只是协助调试用的额外信息,所以不能使用name属性的值来获取对于函数的引用
6. 明确函数的多重用途
ES5及早期版本中的函数具有多重功能,可以结合new使用,函数内的this值将指向一个新对象,函数最终会返回这个新对象。
function Person(name){
this.name = name;
}
var person = new Person("HaHa");
var noAPerson = Person("HaHa");
console.log(person); // Person {name: "HaHa"}
console.log(noAPerson); // undefined
console.log(name); // HaHa
给noAPerson赋值时,没有通过new关键字来调用Person(),最终返回undefined(因为是非严格模式,全局下会设置name属性,严格模式下会直接报错)。通过new关键字调用Person()时才体现其能力。
在ES6中,函数混乱的双重身份终于将有一些改变。
JavaScript函数有两个不同的内部方法[[Call]]和[[Construct]]。当通过new关键字调用时执行的是[[Construct]]函数,它负责创建一个通常称为治理的新对象,然后在执行函数体,将this绑定到示例上;如果不通过new来调用函数,则执行[[Call]]函数,从而直接执行代码中的函数体。具有[[Construct]]方法的函数被统称为构造函数。
切记,不是所有函数都有[[Construct]]方法,因此不是所有函数都可以用new来调用,比如箭头函数。
ES5中判断函数被调用的方法
为了确定函数是被new调用,通常使用instancsof,但是也不完全可靠。
function Person(name) {
if (this instanceof Person) {
this.name = name;
} else {
return new Person(name);
// 或者直接抛出错误
// throw new Error("必须通过new关键字来调用")
}
}
var person = new Person("HaHa");
var person2 = Person("HaHa");
console.log(person); // Person {name: "HaHa"}
console.log(person2); // Person {name: "HaHa"}
// 下面这种写法会错误的执行,而且会修改person的值
var noAPerson = Person.call(person, "HeiHei");
console.log(noAPerson); // undefined
console.log(person); // Person {name: "HaHa"}
ES6 元属性(Metaproperty)new.target
为了解决判断函数是否通过new关键字调用的问题,ES6引入了new.target这个元属性。元属性是指非对象的属性,其可以提供非对象目标的补充信息(例如new)。当调用函数的[[Construct]]方法时,new.target被赋值为new操作符的目标,通常是新创建对象实例,也就是函数体内this的构造函数;如果调用[[Call]]方法,则new.target的值为undefined。
function Person(name){
if(typeof new.target === Person){
this.name = name;
} else{
throw new Error("必须通过new关键字来调用");
}
}
var person = new Person("haha");
console.log(person); // Person {name: "haha"}
var notAPerson = Person.call(person, "HeiHei"); // Error: 必须通过new关键字来调用
在函数外使用new.target是一个语法错误
7. 块级函数
在ES3和早起版本中,在代码块中声明一个块级函数严格来说是一个语法错误,但是所有的浏览器仍然支持这个特性。但是很不幸,每个浏览器对这个特性的支持都稍有不同,所以最好不要使用这个特性(最好的选择是使用函数表达式)。
为了遏制这种互相不兼容的行为,ES5严格模式中引入了一个错误提示,当在代码块内部声明函数时程序会抛出错误:
"use strict";
if(1){
// 在ES5中抛出语法错误,在ES6中不报错
function doSomething(){}
}
在ES6中,会把doSomething视作一个块级声明,从而可以在定义该函数的代码块内访问和调用它。
在定义函数的代码块内,块级函数会提升至顶部
"use strict";
if (1) {
console.log(typeof doSomething); // function
function doSomething() { }
doSomething();
}
console.log(typeof doSomething); // undefined
但是let定义的函数表达式不会提升至顶部
"use strict";
if (1) {
console.log(typeof doSomething); // ReferenceError: doSomething is not defined
let doSomething = function () { }
doSomething();
}
console.log(typeof doSomething);
非严格模式下的块级函数
与严格模式下稍有不同,这些函数不在提升至代码块的顶部,而是提升至外围函数或全局作用域的顶部
if (1) {
console.log(typeof doSomething); // function
function doSomething() { }
doSomething();
}
console.log(typeof doSomething); // function
8. 箭头函数
箭头函数与传统的JavaScript函数有些许不同,主要集中在以下方面:
- 没有this、super、arguments和new.target绑定 箭头函数中的这些值由外围最近一层非建投函数决定。
- 不能通过new调用 箭头函数没有[[Construct]]方法,所以不能被用作构造函数,如果通过new关键字调用建投函数,程序会报错。
- 没有原型 由于不可以通过new方法调用,因而没有构建原型的需求,所以箭头函数不存在prototype这个属性。
- 不可以改变this的绑定 函数内部的this值不可以被改变,在函数的生命周期内始终保持一致。
- 不支持arguments对象
- 不支持重复的命名参数 无论在严格还是费严格模式下,建投函数都不支持重复的命名参数;而在传统函数的规定中,只有在严格模式下才会不能有重复的命名参数。
箭头函数同样也有一个name属性,这与其他函数的规则相同。
箭头函数语法
当箭头函数只有一个参数时,可以直接写参数名,箭头紧随其后,箭头右侧的表达式被求值后便立即返回,即使没有显示的返回语句,这个箭头函数也可以返回传入的第一个参数,不需要更多的语法铺垫。
let reflect = value => value;
// 相当于(babel转换后的代码)
var reflect = function reflect(value) {
return value;
};
如果传入两个或两个以上的参数,要在参数的两侧添加一对小括号:
let sum = (num1, num2) => num1 + num2;
// 相当于
var sum = function sum(num1, num2) {
return num1 + num2;
};
如果没有参数也要在声明的时候写一个小括号
let getNmae = () => "NowhereToRun";
// 相当于
var getNmae = function getNmae() {
return "NowhereToRun";
};
如果函数有多个语句,可以像传统的函数体一样使用花括号包裹起来
let sum2 = (num1, num2) => {
let temp = num1 + num2;
return temp * num1;
}
// 相当于
var sum2 = function sum2(num1, num2) {
var temp = num1 + num2;
return temp * num1;
};
空函数
let doNothing = () => { };
// 相当于
var doNothing = function doNothing() {};
想在箭头函数外返回一个对象字面量,则需要将该字面量包裹在小括号里(为了将其与函数体区分开来)
let getTempItem = id => ({ id: id, name: "Temp" });
// 相当于
var getTempItem = function getTempItem(id) {
return { id: id, name: "Temp" };
};
箭头函数没有this绑定
下面这段代码PageHandler设计初衷是用来处理页面上的交互,init初始化。
let PageHandler = {
id : "123456",
init: function(){
document.addEventListener('click', function(event){
this.doSomething(event.type); // Uncaught TypeError: this.doSomething is not a function
}, false);
},
doSomething: function(type){
console.log("Handling " + type + " for " + this.id);
}
};
但是this.doSomething(event.type);
中的this绑定的是document
(因为是document负责了调用)。可以使用bind强行绑定
let PageHandler = {
id : "123456",
init: function(){
document.addEventListener('click', (function(event){
this.doSomething(event.type);
}).bind(this), false);
},
doSomething: function(type){
console.log("Handling " + type + " for " + this.id);
}
};
使用bind总觉的有些奇怪,因为他实际上创建了另一个函数。可以使用箭头函数来修正。
箭头函数没有this绑定,必须通过查找作用域链来决定其值。如果箭头函数被非箭头函数包含,this绑定的是最近一层费箭头函数的this。否则this的值会被设置为undefined。
let PageHandler = {
id: "123456",
init: function () {
document.addEventListener('click', event =>
this.doSomething(event.type)
, false);
},
doSomething: function (type) {
console.log("Handling " + type + " for " + this.id);
}
};
此处的this和init函数里的this一致。
箭头函数缺少正常函数所拥有的prototype属性,它的设计初衷是“即用即弃”,所以不能用他来定义新的类型。
var MyType = () => {};
new MyType(); // MyType is not a constructor
同样,箭头函数中的this值取决于该函数外部非箭头函数的this值,且不能通过call、apply、或bind方法来改变this值。(使用不会报错,但是无效)
箭头函数没有arguments绑定
箭头函数没有自己的arguments对象,且未来无论在哪个上下文中执行,箭头函数始终可以访问外围函数的arguments对象。
function createArrowFunctionReturningFirstArg(){
return () => arguments[0];
}
var arrowFunction = createArrowFunctionReturningFirstArg(5);
console.log(arrowFunction()); // 5
8. 尾调用优化
ES6关于函数最有趣的变化可能是尾调用系统的引擎优化。
function doSomething (){
return doSomethingElse(); // 尾调用
}
在ES5中,尾调用的实现与其他函数小勇的实现类似:创建一个新的栈帧(stack frame),将其推入调用栈来表示函数调用。也就是说,在循环调用中,每一个未用完的帧都会被保存在内存中,当调用站变得过大时会造成程序问题。
ES6中的尾调用优化
ES6中缩减了严格模式下尾调用栈的大小(非严格模式下不受影响),如果满足一下条件,尾调用不再创建新的栈帧,而是清除并重用当前栈帧:
- 尾调用不访问当前栈帧的变量(也就是说函数不是一个闭包)。
- 在函数内部,尾调用是最后一条语句。
- 尾调用的结果作为函数值返回。
以下代码满足以上三个条件,可以被JavaScript引擎自动优化:
"use strict";
function doSomething (){
return doSomethingElse();
}
如果做一个小的改动,不返回最终结果,那么引擎就无法优化当前函数:
"use strict";
function doSomething (){
doSomethingElse();
}
同样地,如果你定义了一个函数,在尾调用返回后执行其他操作,则函数也无法得到优化:
"use strict";
function doSomething (){
return 1 + doSomethingElse();
}
在上面这个示例中,在返回doSomethingElse()的结果前将其加1,折足以去优化空间。
还有另外一种意外情况,如果把函数调用的结果存储在一个变量里,最后再返回这个变量,则可能导致引擎无法优化:
function doSomething (){
var result = doSomethingElse();
return result;
}
可能最难避免的情况是闭包的使用,它可以访问作用域中的所有变量,因而导致尾调用优化失效:
"use strict";
function doSomething() {
var num = 1,
func = () => num;
// 无法优化,这是一个闭包
return func();
}
如何利用尾调用优化
实际上,尾调用的优化发生在引擎背后,除非你尝试优化一个函数,否则无需思考此类问题。递归函数是其最主要的应用场景,此时尾调用优化的效果最显著。
"use strict";
function factorial(n) {
if (n <= 1) {
return 1;
} else {
return n * factorial(n - 1);
}
}
由于在递归时执行了乘法操作,因而当前版本的阶乘函数无法被引擎优化。如果n是一个非常大的数,则调用栈的尺寸就会不断增长并存在最终导致栈溢出的潜在风险。
优化这个函数,首先要确保乘法不会在函数调用后执行,你可以通过默认参数来将乘法操作移除return语句,结果函数可以携带着临时结果进入到下一个迭代中。下面这段代码可以被ES6引擎优化:
"use strict";
function factorial(n, p = 1) {
if (n <= 1) {
return 1 * p;
} else {
let result = n * p;
return factorial(n - 1, result);
}
}