debugger 语句

debugger 语句调用任何可用的调试功能,例如设置断点。 如果没有调试功能可用,则此语句不起作用。
可以用来在 chrome 中的 console调试

try 和 throw 语句

try…catch语句标记要尝试的语句块,并指定一个出现异常时抛出的响应。

  1. try {
  2. nonExistentFunction();
  3. }
  4. catch(error) {
  5. console.error(error);
  6. // expected output: ReferenceError: nonExistentFunction is not defined
  7. // Note - error messages will vary depending on browser
  8. }

语法

  1. try {
  2. try_statements
  3. }
  4. [catch (exception_var_1 if condition_1) { // non-standard
  5. catch_statements_1
  6. }]
  7. ...
  8. [catch (exception_var_2) {
  9. catch_statements_2
  10. }]
  11. [finally {
  12. finally_statements
  13. }]

try_statements需要被执行的语句。catch_statements_1, catch_statements_2如果在try块里有异常被抛出时执行的语句。exception_var_1, exception_var_2用于保存关联catch子句的异常对象的标识符。condition_1一个条件表达式。finally_statements在try语句块之后执行的语句块。无论是否有异常抛出或捕获这些语句都将执行。

catch 块

  • 如果在try块中有任何一个语句(或者从try块中调用的函数)抛出异常,后面代码不会执行,控制立即转向catch子句
  1. // aaa()执行异常了,后面代码不执行
  2. try{
  3. aaa()
  4. bbb()
  5. } catch (e) {
  6. console.log(e)
  7. } finally {
  8. console.log('finally')
  9. }
  10. ReferenceError: "aaa is not defined"
  11. finally
  1. function isValidJSON(text) {
  2. try {
  3. JSON.parse(text);
  4. return true;
  5. } catch {
  6. return false;
  7. }
  8. }
  9. JSON.parse(text);如果执行异常了,return 语句也不会执行
  • 如果在try块中没有异常抛出,会跳过catch子句。
  • 嵌套一个 try 语句,如果内部的try语句没有catch子句,那么将会进入包裹它的try语句的catch子句。
  1. try{
  2. try{
  3. aaaa()
  4. } catch (e) {
  5. console.log(e)
  6. } finally {
  7. console.log('finally111')
  8. }
  9. dddd()
  10. } catch (e) {
  11. console.log(e)
  12. } finally {
  13. console.log('finally')
  14. }
  15. // 结果
  16. ReferenceError: "aaaa is not defined"
  17. finally111
  18. ReferenceError: "dddd is not defined"
  19. finally
  20. aaaa()出错了,被捕捉了,继续执行 dddd()
  21. try{
  22. try{
  23. aaaa()
  24. } finally {
  25. console.log('finally111')
  26. }
  27. dddd()
  28. } catch (e) {
  29. console.log(e)
  30. } finally {
  31. console.log('finally')
  32. }
  33. // 结果
  34. finally111
  35. ReferenceError: "aaaa is not defined"
  36. finally
  37. aaaa()出错没有被捕捉,跳过 dddd(),进入 catch 子句
  38. try{
  39. try{
  40. aaaa()
  41. } catch (e) {
  42. console.log(e)
  43. throw e;
  44. } finally {
  45. console.log('finally111')
  46. }
  47. } catch (e) {
  48. console.log(e)
  49. } finally {
  50. console.log('finally')
  51. }
  52. // 结果
  53. ReferenceError: "aaaa is not defined"
  54. finally111
  55. ReferenceError: "aaaa is not defined"
  56. finally
  57. aaaa()出错没有被捕捉,跳过 dddd(),进入 catch 子句
  • 如果 catch 块中有 return
  1. function foo() {
  2. try {
  3. a()
  4. } catch {
  5. return 'return'
  6. console.log('catch')
  7. } finally {
  8. console.log('finally')
  9. }
  10. }
  11. console.log(foo())
  12. finally
  13. return
  14. // catch中有 return,catch 后面的语句不执行,finally 还是会执行

异常标识符

catch 块指定一个标识符,应该是用 with 添加到 catch 块,标识符尽在 catch块执行时存在。标识符有 throw 指定

  1. try {
  2. throw "myException"; // generates an exception
  3. }
  4. catch (e) {
  5. // statements to handle any exceptions
  6. logMyErrors(e); // pass exception object to error handler
  7. }

finally块

  • try 和catch块中无论是否抛出异常,是否有 return,finally 子句都会执行
  1. function foo1() {
  2. try {
  3. a()
  4. throw 'try'
  5. return 'try'
  6. } catch {
  7. return 'catch'
  8. throw 'catch'
  9. } finally {
  10. console.log('finally')
  11. }
  12. }
  13. foo1()
  14. // output:
  15. // finally
  • 如果从finally块中返回一个值,那么这个值将会成为整个try-catch-finally的返回值,无论是否有return语句在try和catch中。这包括在catch块里抛出的异常。
  1. function foo1() {
  2. try {
  3. try {
  4. a()
  5. throw 'try'
  6. return 'try'
  7. } catch {
  8. return 'catch'
  9. throw 'catch'
  10. } finally {
  11. return 'finally'
  12. }
  13. } catch (e) {
  14. console.log('outer', e.message)
  15. }
  16. }
  17. foo1()
  18. // output:
  19. // finally

throw语句

throw语句用来抛出一个用户自定义的异常。当前函数的执行将被停止(throw之后的语句将不会执行),并且控制将被传递到调用堆栈中的第一个catch块。如果调用者函数中没有catch块,程序将会终止。

抛出一个对象

  1. function UserException(message) {
  2. this.message = message;
  3. this.name = "UserException";
  4. }
  5. try {
  6. throw new UserException('xxx')
  7. } catch (e){
  8. if (e instanceof UserException) {
  9. console.log(e)
  10. }
  11. }

重新抛出异常

你可以使用throw来抛出异常。下面的例子捕捉了一个异常值为数字的异常,并在其值大于50后重新抛出异常。重新抛出的异常传播到闭包函数或顶层,以便用户看到它。

  1. try {
  2. throw n; // 抛出一个数值异常
  3. } catch (e) {
  4. if (e <= 50) {
  5. // 异常在 1-50 之间时,直接处理
  6. } else {
  7. // 异常无法处理,重新抛出
  8. throw e;
  9. }
  10. }

Labelled 语句

标记语句只能和 break 或 continue 语句一起使用。标记就是在一条语句前面加个可以引用的标识符(identifier)。

  1. let str = "";
  2. loop:
  3. for (let i = 0; i < 2; i++) {
  4. for (let i = 0; i < 6; i++) {
  5. if (i === 1) {
  6. continue loop;
  7. }
  8. str = str + i;
  9. }
  10. }
  11. console.log(str);
  12. // expected output: "00"

在标记块中使用 break

你可以在代码块中使用标记,但只有 break 语句可以使用非循环标记

  1. foo: {
  2. console.log('face');
  3. break foo;
  4. console.log('this will not be executed');
  5. }
  6. console.log('swap');
  7. // this will log:
  8. // "face"
  9. // "swap

标记函数申明

严格模式无法标记函数申明,无论是否处于严格模式下,生成器函数都不能被标记:

  1. 'use strict';
  2. L: function F() {}
  3. // SyntaxError: functions cannot be labelled
  4. L: function* F() {}
  5. // SyntaxError: generator functions cannot be labelled

with

是块作用域的一 个例子(块作用域的一种形式),用 with 从对象中创建出的作用域仅在 with 声明中而非外 部作用域中有效。

  1. let o = {a:1, b:2}
  2. with(o){
  3. console.log(a, b);
  4. }

with语句把对象的属性在它内部的作用域内变成变量。

return

return语句终止函数的执行,并返回一个指定的值给函数调用者。

语法

  1. return [[expression]];

expression表达式的值会被返回。如果忽略,则返回 undefined。

中断一个函数的执行

函数将会在return语句执行后立即中止。

返回一个函数

返回一个函数就是闭包了

break和 continue 语句

break 语句中止当前循环,switch语句或label 语句,并把程序控制流转到紧接着被中止语句后面的语句。continue语句用于结束本次循环并继续循环。
需要注意的是,它们都有带标签的用法。

  1. outer:for(let i = 0; i < 100; i++)
  2. inner:for(let j = 0; j < 100; j++)
  3. if( i == 50 && j == 50)
  4. break outer;
  5. outer:for(let i = 0; i < 100; i++)
  6. inner:for(let j = 0; j < 100; j++)
  7. if( i >= 50 && j == 50)
  8. continue outer;

带标签的break和continue可以控制自己被外层的哪个语句结构消费,这可以跳出复杂的语句结构。

表达式语句

Iteration语句

do while 和 while

  1. let a = 10
  2. while(a--) {
  3. console.log(a);
  4. }
  5. // returns 9-0
  6. while(a--) {
  7. console.log(a);
  8. }
  9. // returns 9-1
  10. let a = 10;
  11. do {
  12. console.log(a);
  13. } while(a--)
  14. // returns 10-0
  15. do {
  16. console.log(a);
  17. } while(a--)
  18. // returns 10-1

for

执行的顺序是初始化,然后是判断,如果 true 执行,如果 false停止,第二次执行表达式,继续判断
initialization
一个表达式 (包含赋值语句) 或者变量声明。典型地被用于初始化一个计数器。该表达式可以使用 varlet 关键字声明新的变量,使用 var 声明的变量不是该循环的局部变量,而是与 for 循环处在同样的作用域中。用 let 声明的变量是语句的局部变量。该表达式的结果无意义。
condition
一个条件表达式被用于确定每一次循环是否能被执行。如果该表达式的结果为 true,statement 将被执行。这个表达式是可选的。如果被忽略,那么就被认为永远为真。如果计算结果为假,那么执行流程将被跳到 for 语句结构后面的第一条语句。
final-expression每次循环的最后都要执行的表达式。执行时机是在下一次 condition 的计算之前。通常被用于更新或者递增计数器变量。

for in

for...in语句以任意顺序遍历一个对象的除Symbol以外的可枚举属性。

只遍历可枚举属性(包括它的原型链上的可枚举属性),内置的属性不可枚举,所以不会被遍历到

  1. var myobj = {a:1,b:2};
  2. myobj.name="hello";
  3. Object.prototype.method=function(){
  4. console.log("world");
  5. }
  6. for (var key in myobj) {
  7. console.log(key);
  8. }
  9. // a b name method
  10. // 自己添加的可枚举属性会被遍历到

仅迭代自身的属性

  1. for (var key in myobj) {
  2. // i 是字符串
  3. if (myobj.hasOwnProperty(key)) {
  4. console.log(key);
  5. }
  6. }

当然数组也是对象,也可以使用 for … in,但是会被当成对象,所以遍历出来的 key 是字符串,还有数组的不是索引的其他值也会被遍历

  1. var arr = [1,2,3];
  2. arr.namekey = 'namevalue';
  3. for (var key in arr) {
  4. console.log(key,arr[key])
  5. }
  6. // 为什么不会遍历出 length呢
  7. arr.length // 3
  8. Object.getOwnPropertyDescriptor(arr,'length')
  9. length:
  10. value: 3
  11. writable: true
  12. enumerable: false
  13. configurable: false

不推荐变量数组,原因

  1. index的数据类型是string并不是number
  2. 不能保证for ... in将以任何特定的顺序返回索引
  3. for ... in循环语句将返回所有可枚举属性,包括非整数类型的名称和继承的那些。

for of

for...of语句可迭代对象,上创建一个迭代循环,调用自定义迭代钩子,并为每个不同属性的值执行语句

  1. const prices = {
  2. A: [1,2,4],
  3. B: [3,4,5]
  4. }
  5. // todo 如何修改遍历机制
  6. for (let key in prices) {
  7. console.log(prices[key])
  8. }

一个数据结构只要部署了Symbol.iterator属性,就被视为具有 iterator 接口,就可以用for...of循环遍历它的成员。也就是说,for...of循环内部调用的是数据结构的Symbol.iterator方法。
for...of循环可以使用的范围包括数组、Set 和 Map 结构、String,TypeArray,某些类似数组的对象(比如arguments对象、DOM NodeList 对象)、Generator 对象。

数组

数组原生具备iterator接口(即默认部署了Symbol.iterator属性),for...of循环本质上就是调用这个接口产生的遍历器,可以用下面的代码证明。

  1. const arr = ['red', 'green', 'blue'];
  2. for(let v of arr) {
  3. console.log(v); // red green blue
  4. }
  5. const obj = {};
  6. obj[Symbol.iterator] = arr[Symbol.iterator].bind(arr);
  7. for(let v of obj) {
  8. console.log(v); // red green blue
  9. }

上面代码中,空对象obj部署了数组arrSymbol.iterator属性,结果objfor...of循环,产生了与arr完全一样的结果。
for...of循环可以代替数组实例的forEach方法。

  1. const arr = ['red', 'green', 'blue'];
  2. arr.forEach(function (element, index) {
  3. console.log(element); // red green blue
  4. console.log(index); // 0 1 2
  5. });

JavaScript 原有的for...in循环,只能获得对象的键名,不能直接获取键值。ES6 提供for...of循环,允许遍历获得键值。

  1. var arr = ['a', 'b', 'c', 'd'];
  2. for (let a in arr) {
  3. console.log(a); // 0 1 2 3
  4. }
  5. for (let a of arr) {
  6. console.log(a); // a b c d
  7. }

上面代码表明,for...in循环读取键名,for...of循环读取键值。如果要通过for...of循环,获取数组的索引,可以借助数组实例的entries方法和keys方法(参见《数组的扩展》一章)。
for...of循环调用遍历器接口,数组的遍历器接口只返回具有数字索引的属性。这一点跟for...in循环也不一样。

  1. let arr = [3, 5, 7];
  2. arr.foo = 'hello';
  3. for (let i in arr) {
  4. console.log(i); // "0", "1", "2", "foo"
  5. }
  6. for (let i of arr) {
  7. console.log(i); // "3", "5", "7"
  8. }

上面代码中,for...of循环不会返回数组arrfoo属性。

Set 和 Map 结构

Set 和 Map 结构也原生具有 Iterator 接口,可以直接使用for...of循环。

  1. var engines = new Set(["Gecko", "Trident", "Webkit", "Webkit"]);
  2. for (var e of engines) {
  3. console.log(e);
  4. }
  5. // Gecko
  6. // Trident
  7. // Webkit
  8. var es6 = new Map();
  9. es6.set("edition", 6);
  10. es6.set("committee", "TC39");
  11. es6.set("standard", "ECMA-262");
  12. for (var [name, value] of es6) {
  13. console.log(name + ": " + value);
  14. }
  15. // edition: 6
  16. // committee: TC39
  17. // standard: ECMA-262

上面代码演示了如何遍历 Set 结构和 Map 结构。值得注意的地方有两个,首先,遍历的顺序是按照各个成员被添加进数据结构的顺序。其次,Set 结构遍历时,返回的是一个值,而 Map 结构遍历时,返回的是一个数组,该数组的两个成员分别为当前 Map 成员的键名和键值。

  1. let map = new Map().set('a', 1).set('b', 2);
  2. for (let pair of map) {
  3. console.log(pair);
  4. }
  5. // ['a', 1]
  6. // ['b', 2]
  7. for (let [key, value] of map) {
  8. console.log(key + ' : ' + value);
  9. }
  10. // a : 1
  11. // b : 2

计算生成的数据结构

有些数据结构是在现有数据结构的基础上,计算生成的。比如,ES6 的数组、Set、Map 都部署了以下三个方法,调用后都返回遍历器对象。

  • entries() 返回一个遍历器对象,用来遍历[键名, 键值]组成的数组。对于数组,键名就是索引值;对于 Set,键名与键值相同。Map 结构的 Iterator 接口,默认就是调用entries方法。
  • keys() 返回一个遍历器对象,用来遍历所有的键名。
  • values() 返回一个遍历器对象,用来遍历所有的键值。

这三个方法调用后生成的遍历器对象,所遍历的都是计算生成的数据结构。

  1. let arr = ['a', 'b', 'c'];
  2. for (let pair of arr.entries()) {
  3. console.log(pair);
  4. }
  5. // [0, 'a']
  6. // [1, 'b']
  7. // [2, 'c']

类似数组的对象

类似数组的对象包括好几类。下面是for...of循环用于字符串、DOM NodeList 对象、arguments对象的例子。

  1. // 字符串
  2. let str = "hello";
  3. for (let s of str) {
  4. console.log(s); // h e l l o
  5. }
  6. // DOM NodeList对象
  7. let paras = document.querySelectorAll("p");
  8. for (let p of paras) {
  9. p.classList.add("test");
  10. }
  11. // arguments对象
  12. function printArgs() {
  13. for (let x of arguments) {
  14. console.log(x);
  15. }
  16. }
  17. printArgs('a', 'b');
  18. // 'a'
  19. // 'b'

对于字符串来说,for...of循环还有一个特点,就是会正确识别 32 位 UTF-16 字符。

  1. for (let x of 'a\uD83D\uDC0A') {
  2. console.log(x);
  3. }
  4. // 'a'
  5. // '\uD83D\uDC0A'

并不是所有类似数组的对象都具有 Iterator 接口,一个简便的解决方法,就是使用Array.from方法将其转为数组。

  1. let arrayLike = { length: 2, 0: 'a', 1: 'b' };
  2. // 报错
  3. for (let x of arrayLike) {
  4. console.log(x);
  5. }
  6. // 正确
  7. for (let x of Array.from(arrayLike)) {
  8. console.log(x);
  9. }

对象

对于普通的对象,for...of结构不能直接使用,会报错,必须部署了 Iterator 接口后才能使用。但是,这样情况下,for...in循环依然可以用来遍历键名。

  1. let es6 = {
  2. edition: 6,
  3. committee: "TC39",
  4. standard: "ECMA-262"
  5. };
  6. for (let e in es6) {
  7. console.log(e);
  8. }
  9. // edition
  10. // committee
  11. // standard
  12. for (let e of es6) {
  13. console.log(e);
  14. }
  15. // TypeError: es6[Symbol.iterator] is not a function

上面代码表示,对于普通的对象,for...in循环可以遍历键名,for...of循环会报错。
一种解决方法是,使用Object.keys方法将对象的键名生成一个数组,然后遍历这个数组。

  1. for (var key of Object.keys(someObject)) {
  2. console.log(key + ': ' + someObject[key]);
  3. }

另一个方法是使用 Generator 函数将对象重新包装一下。

  1. function* entries(obj) {
  2. for (let key of Object.keys(obj)) {
  3. yield [key, obj[key]];
  4. }
  5. }
  6. for (let [key, value] of entries(obj)) {
  7. console.log(key, '->', value);
  8. }
  9. // a -> 1
  10. // b -> 2
  11. // c -> 3

for await of

迭代异步可迭代对象

你还可以迭代一个明确实现异步迭代协议的对象:

  1. var asyncIterable = {
  2. [Symbol.asyncIterator]() {
  3. return {
  4. i: 0,
  5. next() {
  6. if (this.i < 3) {
  7. return Promise.resolve({ value: this.i++, done: false });
  8. }
  9. return Promise.resolve({ done: true });
  10. }
  11. };
  12. }
  13. };
  14. (async function() {
  15. for await (num of asyncIterable) {
  16. console.log(num);
  17. }
  18. })();
  19. // 0
  20. // 1
  21. // 2

迭代异步生成器

异步生成器已经实现了异步迭代器协议, 所以可以用 for await...of循环。

  1. async function* asyncGenerator() {
  2. var i = 0;
  3. while (i < 3) {
  4. yield i++;
  5. }
  6. }
  7. (async function() {
  8. for await (num of asyncGenerator()) {
  9. console.log(num);
  10. }
  11. })();
  12. // 0
  13. // 1
  14. // 2

有关使用for await... of考虑迭代API中获取数据的异步 generator 更具体的例子。这个例子首先为一个数据流创建了一个异步 generator,然后使用它来获得这个API的响应值的大小。

  1. async function* streamAsyncIterator(stream) {
  2. const reader = stream.getReader();
  3. try {
  4. while (true) {
  5. const { done, value } = await reader.read();
  6. if (done) {
  7. return;
  8. }
  9. yield value;
  10. }
  11. } finally {
  12. reader.releaseLock();
  13. }
  14. }
  15. // 从url获取数据并使用异步 generator 来计算响应值的大小
  16. async function getResponseSize(url) {
  17. const response = await fetch(url);
  18. // Will hold the size of the response, in bytes.
  19. let responseSize = 0;
  20. // 使用for-await-of循环. 异步 generator 会遍历响应值的每一部分
  21. for await (const chunk of streamAsyncIterator(response.body)) {
  22. // Incrementing the total response length.
  23. responseSize += chunk.length;
  24. }
  25. console.log(`Response Size: ${responseSize} bytes`);
  26. // expected output: "Response Size: 1071472"
  27. return responseSize;
  28. }
  29. getResponseSize('https://jsonplaceholder.typicode.com/photos');

迭代算法比较

以数组为例,JavaScript 提供多种遍历语法。最原始的写法就是for循环。

  1. for (var index = 0; index < myArray.length; index++) {
  2. console.log(myArray[index]);
  3. }

这种写法比较麻烦,因此数组提供内置的forEach方法。

  1. myArray.forEach(function (value) {
  2. console.log(value);
  3. });

这种写法的问题在于,无法中途跳出forEach循环,break命令或return命令都不能奏效。
for...in循环可以遍历数组的键名。

  1. for (var index in myArray) {
  2. console.log(myArray[index]);
  3. }

for...in循环有几个缺点。

  • 数组的键名是数字,但是for...in循环是以字符串作为键名“0”、“1”、“2”等等。
  • for...in循环不仅遍历数字键名,还会遍历手动添加的其他键,甚至包括原型链上的键。
  • 某些情况下,for...in循环会以任意顺序遍历键名。

总之,for...in循环主要是为遍历对象而设计的,不适用于遍历数组。
for...of循环相比上面几种做法,有一些显著的优点。

  1. for (let value of myArray) {
  2. console.log(value);
  3. }
  • 有着同for...in一样的简洁语法,但是没有for...in那些缺点。
  • 不同于forEach方法,它可以与breakcontinuereturn配合使用。
  • 提供了遍历所有数据结构的统一操作接口。

下面是一个使用 break 语句,跳出for...of循环的例子。

  1. for (var n of fibonacci) {
  2. if (n > 1000)
  3. break;
  4. console.log(n);
  5. }

上面的例子,会输出斐波纳契数列小于等于 1000 的项。如果当前项大于 1000,就会使用break语句跳出for...of循环。

条件判断语句

if…else

  1. if (true) {
  2. let a = 10;
  3. }
  4. console.log(a) // a is not defined

我们知道 if 可以没有大括号,

  1. if (true) let a = 10; // Uncaught SyntaxError: Lexical declaration cannot appear in a single-statement context

不要将原始布尔值的true和false与Boolean对象的真或假混淆。任何一个值,只要它不是 undefined、null、 0、NaN或空字符串(””),那么无论是任何对象,即使是值为假的Boolean对象,在条件语句中都为真。例如:

  1. var b = new Boolean(false);
  2. if (b) //表达式的值为true

switch

  1. switch(1) {
  2. case 1:
  3. console.log(1)
  4. case 2:
  5. console.log(2)
  6. case 3:
  7. console.log(3)
  8. }
  9. // 1,2,3

如果我们要把它变成分支型,则需要在每个case后加上break。

空语句

空语句就是一个独立的分号,实际上没什么大用。我们来看一下:

  1. ;

空语句的存在仅仅是从语言设计完备性的角度考虑,允许插入多个分号而不抛出错误

Block

块语句(或其他语言的复合语句)用于组合零个或多个语句。该块由一对大括号界定,可以是labelled:

声明和变量语句

var

var声明的变量是函数作用域,会定义到全局对象上

  1. var a; // undefined 如果未指定初始值,初始值就是
  2. var i = 0, j = 0, k = 0;
  3. for (var p in o) console.log(p);//定义在循环体内使用的变量,如果用 var 定义,其实作用域在外部,全局或是函数

在函数体内定义的 var变量是局部变量

  1. function test() {
  2. var message = "hi"; // local variable
  3. }
  4. test();
  5. console.log(message); // error!

没有用 var 定义的变量是全局变量

  1. function test() {
  2. message = "hi"; // global variable
  3. }
  4. test();
  5. console.log(message); // "hi"

var定义的变量存在变量提升,根预解析有关。
在执行代码前,预解析,把所有 var 定义的变量放到词法作用域里面的变量环境,设置为 undefined。可以重名

如果我们仍然想要使用var,我的个人建议是,把它当做一种“保障变量是局部”的逻辑

  • 声明同时必定初始化;
  • 尽可能在离使用的位置近处声明;
  • 不要在意重复声明。
  1. var x = 1, y = 2;
  2. doSth(x, y);
  3. for(var x = 0; x < 10; x++)
  4. doSth2(x);

改成

  1. {
  2. let x = 1, y = 2;
  3. doSth(x, y);
  4. }
  5. for(let x = 0; x < 10; x++)
  6. doSth2(x);

let

let声明的变量绑定到块作用域。不会成为全局对象的属性

不能重复声明,第一次声明的时候是 uninitial,存在执行上下文的词法作用域的词法环境

隐式的绑定

  1. var foo = true;
  2. if (foo) {
  3. let bar = foo * 2;
  4. bar = something( bar ); console.log( bar );
  5. }
  6. console.log( bar ); // ReferenceError

显式的绑定

  1. if (foo) {
  2. {
  3. // <-- 显式的快
  4. let bar = foo * 2;
  5. bar = something( bar ); console.log( bar );
  6. } }
  7. console.log( bar ); // ReferenceError

垃圾回收
使用let 定义的变量,绑定到块作用域,其他作用域访问不到,
方便垃圾回收

  1. function process(data) {
  2. // 在这里做点有趣的事情
  3. }
  4. // 在这个块中定义的内容可以销毁了! {
  5. let someReallyBigData = { .. };
  6. process( someReallyBigData );
  7. }
  8. var btn = document.getElementById( "my_button" );
  9. btn.addEventListener( "click", function click(evt){
  10. console.log("button clicked");
  11. }, /*capturingPhase=*/false );

块级作用域与函数声明

考虑到环境导致的行为差异太大,应该避免在块级作用域内声明函数。如果确实需要,也应该写成函数表达式,而不是函数声明语句。

const

const声明一个只读的常量。一旦声明,常量的值就不能改变。
如果是对象,对象的地址不能变,属性可以增加修改

class

class最基本的用法只需要class关键字、名称和一对大括号。它的声明特征跟const和let类似,都是作用于块级作用域,预处理阶段则会屏蔽外部变量。TDZ
需要注意,class默认内部的函数定义都是strict模式的。

私有变量

严格来讲,JavaScript 中没有私有成员的概念;所有对象属性都是公有的。不过,倒是有一个私有 变量的概念。任何在函数中定义的变量,都可以认为是私有变量,因为不能在函数的外部访问这些变量。 私有变量包括函数的参数、局部变量和在函数内部定义的其他函数。
如果在这个函数内部创建一个闭包,那么闭包通过自己的作用域链也可以访 问这些变量。而利用这一点,就可以创建用于访问私有变量的公有方法。

第一种是在构造函数中定义特权方法

  1. function Person(name){
  2. this.getName = function(){ return name;
  3. };
  4. this.setName = function (value) { name = value;
  5. }; }
  6. var person = new Person("Nicholas"); alert(person.getName()); //"Nicholas" person.setName("Greg"); alert(person.getName()); //"Greg"

构造函数模式的缺点是针对每个实例都会创建同样一组新方法

静态私有变量

  1. (function(){
  2. //私有变量和私有函数
  3. var privateVariable = 10;
  4. function privateFunction(){
  5. return false;
  6. }
  7. //构造函数
  8. MyObject = function(){ };
  9. //公有/特权方法
  10. MyObject.prototype.publicMethod = function(){
  11. privateVariable++;
  12. return privateFunction();
  13. };
  14. })();

这个模式与在构造函数中定义特权方法的主要区别,就在于私有变量和函数是由实例共享的。由于 特权方法是在原型上定义的,因此所有实例都使用同一个函数。而这个特权方法,作为一个闭包,总是 保存着对包含作用域的引用。来看一看下面的代码。

  1. (function(){
  2. var name
  3. Person = name
  4. };
  5. = "";
  6. function(value){ = value;
  7. Person.prototype.getName = function(){ return name;
  8. };
  9. Person.prototype.setName = function (value){
  10. name = value; };
  11. })();
  12. var person1 = new Person("Nicholas"); alert(person1.getName()); //"Nicholas" person1.setName("Greg"); alert(person1.getName()); //"Greg"
  13. var person2 = new Person("Michael"); alert(person1.getName()); //"Michael" alert(person2.getName()); //"Michael"

以这种方式创建静态私有变量会因为使用原型而增进代码复用,但每个实例都没有自己的私有变 量。到

function

判断变量存在

  1. // 错误
  2. aa == undefined // Uncaught ReferenceError: aa is not defined
  3. // 正确
  4. typeof aa === 'undefined' // true

变量作用域(scope)

变量的作用域是词法作用域

顶层对象

ES5 的顶层对象,本身也是一个问题,因为它在各种实现里面是不统一的。

  • 浏览器里面,顶层对象是window,但 Node 和 Web Worker 没有window。
  • 浏览器和 Web Worker 里面,self也指向顶层对象,但是 Node 没有self。
  • Node 里面,顶层对象是global,但其他环境都不支持。

同一段代码为了能够在各种环境,都能取到顶层对象,现在一般是使用this变量,但是有局限性。

  • ES5之中,顶层对象的属性与全局变量是等价的。
  1. window.a = 1;
  2. a // 1
  3. a = 2;
  4. window.a // 2
  5. var bar = 1;
  6. window.bar //1

var命令和function命令声明的全局变量,依旧是顶层对象的属性;

  • es6中let命令、const命令、class命令声明的全局变量,不属于顶层对象的属性。也就是说,从ES6开始,全局变量将逐步与顶层对象的属性脱钩。
  1. let b = 1;
  2. window.b // undefined

全局作用域

正常使用的时候禁止this关键字指向全局对象
严格模式和非严格模式
作用域在严格模式和非严格模式下不同
使用’use strict’
严格模式:

  1. use strict
  2. aa = 10 // aa is not defined

给一个没有声明的变量赋值会报错

非严格模式:

  1. aa = 10 // 10
  2. windows.aa // 10

如果给一个没有声明的变量赋值,js 会给全局对象创建一个同名属性
这个 aa 不是全局变量,aa 是全局对象的一个属性
全局变量是不可以删除的
全局对象的属性是可以删除

  1. var globalVar = 10;
  2. globalVar; // => 10
  3. window.globalVar; // => 10
  4. delete globalVar; // => false
  5. // 严格模式下无法删除变量。只有configurable设置为true的对象属性,才能被删除。
  6. globalVar; // => 10
  7. globalArr = 20;
  8. globalArr; // => 20
  9. window.globalArr; // => 20
  10. delete globalArr; // => true
  11. globalArr; // => undefined

添加一个window 已经存在的变量会报错
Uncaught SyntaxError: Identifier ‘parseFloat’ has already been declared

函数作用域

局部作用域就是函数作用域

  • 函数参数也是局部变量
  1. var scope = 'global'
  2. function checkscope(scope) {
  3. return scope
  4. }
  5. function checkscope1() {
  6. var scope = 'local'
  7. return scope
  8. }
  9. function checkscope2(scope) {
  10. var scope = 'local1'
  11. return scope
  12. }
  13. console.log(checkscope('local')) // local
  14. console.log(checkscope1()) // local
  15. // 函数体内定义的会把参数的覆盖
  16. console.log(checkscope2('local')) // local1
  • 变量提升
  • var
  1. var scope = 'global'
  2. function f () {
  3. console.log(scope)
  4. if (true) {
  5. var scope = 'local'
  6. }
  7. console.log(scope)
  8. }
  9. f() // undefined local
  10. // 用 var 定义没有 tdz,所以是 undefined,不是ReferenceError

相当于

  1. var scope = 'global'
  2. function f () {
  3. var scope;
  4. console.log(scope);
  5. if (true) {
  6. scope = 'local'
  7. }
  8. console.log(scope)
  9. }

js 动态执行前会编译下,编译的时候会把 变量的定义提升到函数作用域顶部
因为有变量提升存在所有用 var 定义的变量没有块作用域

  • let
  1. {
  2. console.log(foo); // variable.js:2 Uncaught ReferenceError: Cannot access 'foo' before initialization
  3. // foo 已经定义,但是在 tdz 区域,引用错误
  4. let foo = 2;
  5. }
  1. {
  2. console.log(a) // a is not defined
  3. }

用 let 定义的变量没有变量提升,从代码块开始,到变量声明的这段区域是,TDZ,在这一区域访问变量都会报ReferenceError错误。

块作用域 block scope

为了加强对变量生命周期的控制,ECMAScript 6 引入了块级作用域。
块级作用域存在于:

  1. 函数内部
  2. 块中(字符 { 和 } 之间的区域)

let 和 const 都是块级声明的一种。

es5

es5的js 没有 block scope,只有 function scope

  1. function test (o) {
  2. var i = 0 // 函数作用域
  3. if (typeof o === 'object') {
  4. var j = 0 // 没有 block scope,是 function scope
  5. for (var k = 0; k < 10; ++k) { // k 也是 function scope
  6. console.log(k)
  7. }
  8. console.log(k)
  9. }
  10. console.log(j)
  11. }
  12. test({})

如果没有块作用域,可以用闭包

  1. var funcs = []
  2. for (var i = 0; i < 10; i++) {
  3. funcs.push(function() {
  4. return i
  5. })
  6. }
  7. funcs.forEach(function(f) {
  8. console.log(f()) // 将在打印10数字10次
  9. })
  10. // i 是全局变量,变量的时候取的都是全局变量 i
  1. // 在JavaScript的闭包中,闭包函数能够访问到包庇函数中的变量,这些闭包函数能够访问到的变量也因此被称为自由变量。只要闭包没有被销毁,那么外部函数将一直在内存中保存着这些变量,在上面的代码中,形参value就是自由变量,return的函数是一个闭包,闭包内部能够访问到自由变量value。同时这儿我们还使用了立即执行函数,立即函数的作用就是在每次迭代的过程中,将i的值作为实参传入立即执行函数,并执行返回一个闭包函数,这个闭包函数保存了外部的自由变量,也就是保存了当次迭代时i的值。
  2. var funcs = []
  3. for (var i = 1; i < 10; i++) {
  4. funcs.push((function(value) {
  5. return function() {
  6. return value
  7. }
  8. })(i))
  9. }
  10. funcs.forEach(function(f) {
  11. console.log(f())
  12. }) // 1...10

es6:

  • 限制在块里面
  1. {
  2. let a = 10
  3. }
  4. console.log(a) // a is not defined
  • 不允许重复定义
  1. {
  2. let a = 10;
  3. let a = 20; // Uncaught SyntaxError: Identifier 'a' has already been declared
  4. }
  • 暂时性死区 tdz
  1. if (true) {
  2. // TDZ开始
  3. tmp = 'abc'; // ReferenceError
  4. console.log(tmp); // ReferenceError
  5. let tmp; // TDZ结束
  6. console.log(tmp); // undefined
  7. tmp = 123;
  8. console.log(tmp); // 123
  9. }

这样的设计是为了让大家养成良好的编程习惯,变量一定要在声明之后使用,否则就报错。
ES6 规定暂时性死区和let、const语句不出现变量提升,主要是为了减少运行时错误,防止在变量声明前就使用这个变量,从而导致意料之外的行为。这样的错误在 ES5 是很常见的,现在有了这种规定,避免此类错误就很容易了。
class 和 let 很像
这是因为 JavaScript 引擎在扫描代码发现变量声明时,要么将它们提升到作用域顶部(遇到 var 声明),要么将声明放在 TDZ 中(遇到 let 和 const 声明)。访问 TDZ 中的变量会触发运行时错误。只有执行过变量声明语句后,变量才会从 TDZ 中移出,然后方可访问。

  • let 声明全局变量,不会成为 window 的属性
  1. var a = 10
  2. let b = 10
  3. console.log(window.a) // 10
  4. console.log(window.b) // undefined

循环中的块作用域

  1. var a = [];
  2. for (var i = 0; i < 10; i++) {
  3. a[i] = function () {
  4. console.log(i);
  5. };
  6. }
  7. a[6](); // 10
  8. // 解决方案
  9. // 闭包
  10. var a = [];
  11. for (var i = 0; i < 10; i++) {
  12. a[i] = (function () {
  13. return function () {console.log(i);}
  14. })(i);
  15. }
  16. a[6](); // 6
  17. // let
  18. var a = [];
  19. for (let i = 0; i < 10; i++) {
  20. a[i] = function () {
  21. console.log(i);
  22. };
  23. }
  24. a[6](); // 6

for循环用 let定义i 变量为什么可以重复定义?每一次循环的i其实都是一个新的变量
另外,for循环还有一个特别之处,就是设置循环变量的那部分是一个父作用域,而循环体内部是一个单独的子作用域。
简单的来说,就是在 for (let i = 0; i < 3; i++) 中,即圆括号之内建立一个隐藏的作用域,然后每次迭代循环时都创建一个新变量,并以之前迭代中同名变量的值将其初始化。

  1. for (var i = 0; i < 3; i++) {
  2. var i = 'abc';
  3. console.log(i);
  4. }
  5. // abc
  6. for (let i = 0; i < 3; i++) {
  7. let i = 'abc';
  8. console.log(i);
  9. }
  10. //输出了3次abc。这表明函数内部的变量i与循环变量i不在同一个作用域,有各自单独的作用域。
  11. // abc
  12. // abc
  13. // abc
  14. // 伪代码
  15. (let i = 0) {
  16. let i = 'abc';
  17. console.log(i);
  18. }
  19. (let i = 1) {
  20. let i = 'abc';
  21. console.log(i);
  22. }
  23. (let i = 2) {
  24. let i = 'abc';
  25. console.log(i);
  26. };

当执行函数的时候,根据词法作用域就可以找到正确的值,其实你也可以理解为 let 声明模仿了闭包的做法来简化循环过程。

循环中的 let 和 const

  1. var funcs = [];
  2. for (const i = 0; i < 10; i++) {
  3. funcs[i] = function () {
  4. console.log(i);
  5. };
  6. }
  7. funcs[0](); // Uncaught TypeError: Assignment to constant variable.

结果会是报错,因为虽然我们每次都创建了一个新的变量,然而我们却在迭代中尝试修改 const 的值,所以最终会报错。

  1. var funcs = [], object = {a: 1, b: 1, c: 1};
  2. for (var key in object) {
  3. funcs.push(function(){
  4. console.log(key)
  5. });
  6. }
  7. funcs[0]()

结果是 ‘c’;

那如果把 var 改成 let 或者 const 呢?

使用 let,结果自然会是 ‘a’,const 呢? 报错还是 ‘a’?

结果是正确打印 ‘a’,这是因为在 for in 循环中,每次迭代不会修改已有的绑定,而是会创建一个新的绑定。

babel

  1. let value = 1;

编译为:

  1. var value = 1;
  1. if (false) {
  2. let value = 1;
  3. }
  4. console.log(value); // Uncaught ReferenceError: value is not defined

编译为

  1. if (false) {
  2. var _value = 1;
  3. }
  4. console.log(value);

作用

块级作用域的出现,实际上使得获得广泛应用的立即执行函数表达式(IIFE)不再必要了。

  1. // IIFE 写法(function () {
  2. var tmp = ...;
  3. ...
  4. }());
  5. // 块级作用域写法
  6. {
  7. let tmp = ...;
  8. ...
  9. }

那循环中的 let 声明呢?

  1. var funcs = [];
  2. for (let i = 0; i < 10; i++) {
  3. funcs[i] = function () {
  4. console.log(i);
  5. };
  6. }
  7. funcs[0](); // 0

Babel 巧妙的编译成了:

  1. var funcs = [];
  2. var _loop = function _loop(i) {
  3. funcs[i] = function () {
  4. console.log(i);
  5. };
  6. };
  7. for (var i = 0; i < 10; i++) {
  8. _loop(i);
  9. }
  10. funcs[0](); // 0

let var const

let 和 var 的区别

  1. let 声明了块作用域,块以外无法访问
  2. let,const的变量提升标注了暂时性死区的开始,使用let命令声明变量之前,该变量都是不可用的
  3. let 不允许重复声明

const

通过const生命的变量将会创建一个对该值的一个只读引用,

而且声明的时候要初始化

  1. const MY_FAV = 7
  2. MY_FAY = 20 // 重复赋值将会报错(Uncaught TypeError: Assignment to constant variable)
  3. const foo = {bar: 'zar'}
  4. foo.bar = 'hello world' // 改变对象的属性并不会报错

通过const生命的对象并不是不可变的。但是在很多场景下,比如在函数式编程中,我们希望声明的变量是不可变的,不论其是原始数据类型还是引用数据类型。

  1. const deepFreeze = function(obj) {
  2. Object.freeze(obj)
  3. for (const key in obj) {
  4. if (typeof obj[key] === 'object') deepFreeze(obj[key])
  5. }
  6. return obj
  7. }
  8. const foo = deepFreeze({
  9. a: {b: 'bar'}
  10. })
  11. foo.a.b = 'zar'
  12. console.log(foo.a.b) // bar

最佳实践

但是,当很多开发者开始将自己的项目迁移到ECMAScript2015后,他们发现,最佳实践应该是,尽可能的使用const,在const不能够满足需求的时候才使用let,永远不要使用var。为什么要尽可能的使用const呢?在JavaScript中,很多bug都是因为无意的改变了某值或者对象而导致的,通过尽可能使用const,或者上面的deepFreeze能够很好地规避这些bug的出现,而我的建议是:如果你喜欢函数式编程,永远不改变已经声明的对象,而是生成一个新的对象,那么对于你来说,const就完全够用了。