JS_ES6基础语法

文章介绍了JavaScriptES6中的let和const命令,强调它们在声明变量时的块级作用域特性,以及与var命令的区别,如变量提升和暂时性死区。此外,文章还讨论了函数声明在块级作用域中的行为,以及const命令声明的常量特性。最后,文章提到了字符串的Unicode表示法和模板字符串的使用。
摘要由CSDN通过智能技术生成

let 和 const 命令

let 命令

ES6 新增了let命令,用来声明变量。它的用法类似于var,但是所声明的变量,只在let命令所在的代码块内有效。

{
  let a = 10;
  var b = 1;
}
​
a // ReferenceError: a is not defined.
b // 1

分别用letvar声明了两个变量。然后在代码块之外调用这两个变量,结果let声明的变量报错,var声明的变量返回了正确的值。这表明,let声明的变量只在它所在的代码块有效。

for循环的计数器,就很合适使用let命令。

for (let i = 0; i < 10; i++) {
  // ...
}
​
console.log(i);
// ReferenceError: i is not defined

不存在变量提升

var命令会发生“变量提升”现象,即变量可以在声明之前使用,值为undefined。这种现象多多少少是有些奇怪的,按照一般的逻辑,变量应该在声明语句之后才可以使用。

为了纠正这种现象,let命令改变了语法行为,它所声明的变量一定要在声明后使用,否则报错。

// var 的情况
console.log(foo); // 输出undefined
var foo = 2;
​
// let 的情况
console.log(bar); // 报错ReferenceError
let bar = 2;

变量foovar命令声明,会发生变量提升,即脚本开始运行时,变量foo已经存在了,但是没有值,所以会输出undefined。变量barlet命令声明,不会发生变量提升。这表示在声明它之前,变量bar是不存在的,这时如果用到它,就会抛出一个错误。

暂时性死区

只要块级作用域内存在let命令,它所声明的变量就“绑定”(binding)这个区域,不再受外部的影响。

var tmp = 123;
​
if (true) {
  tmp = 'abc'; // ReferenceError
  let tmp;
}

存在全局变量tmp,但是块级作用域内let又声明了一个局部变量tmp,导致后者绑定这个块级作用域,所以在let声明变量前,对tmp赋值会报错。

ES6 明确规定,如果区块中存在letconst命令,这个区块对这些命令声明的变量,从一开始就形成了封闭作用域。凡是在声明之前就使用这些变量,就会报错。

总之,在代码块内,使用let命令声明变量之前,该变量都是不可用的。这在语法上,称为“暂时性死区”

if (true) {
  // TDZ开始
  tmp = 'abc'; // ReferenceError
  console.log(tmp); // ReferenceError
​
  let tmp; // TDZ结束
  console.log(tmp); // undefined
​
  tmp = 123;
  console.log(tmp); // 123
}

let命令声明变量tmp之前,都属于变量tmp的“死区”。

“暂时性死区”也意味着typeof不再是一个百分之百安全的操作。

typeof x; // ReferenceError
let x;

变量x使用let命令声明,所以在声明之前,都属于x的“死区”,只要用到该变量就会报错。因此,typeof运行时就会抛出一个ReferenceError

作为比较,如果一个变量根本没有被声明,使用typeof反而不会报错。

typeof undeclared_variable // "undefined"

不允许重复声明

let不允许在相同作用域内,重复声明同一个变量。

// 报错
function func() {
  let a = 10;
  var a = 1;
}
​
// 报错
function func() {
  let a = 10;
  let a = 1;
}

因此,不能在函数内部重新声明参数。

function func(arg) {
  let arg;
}
func() // 报错

function func(arg) {
  {
    let arg;
  }
}
func() // 不报错

块级作用域

let实际上为 JavaScript 新增了块级作用域。

function f1() {
  let n = 5;
  if (true) {
    let n = 10;
  }
  console.log(n); // 5
}

都声明了变量n,运行后输出 5。这表示外层代码块不受内层代码块的影响。如果两次都使用var定义变量n,最后输出的值才是 10。

ES6 允许块级作用域的任意嵌套。

{{{{
  {let insane = 'Hello World'}
  console.log(insane); // 报错
}}}};

上面代码使用了一个五层的块级作用域,每一层都是一个单独的作用域。第四层作用域无法读取第五层作用域的内部变量。

{{{{
  let insane = 'Hello World';
  {let insane = 'Hello World'}
}}}};

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

// IIFE 写法
(function () {
  var tmp = ...;
  ...
}());

// 块级作用域写法
{
  let tmp = ...;
  ...
}

块级作用域与函数声明

ES5 规定,函数只能在顶层作用域和函数作用域之中声明,不能在块级作用域声明。

// 情况一
if (true) {
  function f() {}
}

// 情况二
try {
  function f() {}
} catch(e) {
  // ...
}

上面两种函数声明,根据 ES5 的规定都是非法的。

但是,浏览器没有遵守这个规定,为了兼容以前的旧代码,还是支持在块级作用域之中声明函数,因此上面两种情况实际都能运行,不会报错。

ES6 引入了块级作用域,明确允许在块级作用域之中声明函数。ES6 规定,块级作用域之中,函数声明语句的行为类似于let,在块级作用域之外不可引用。

function f() { console.log('I am outside!'); }

(function () {
  if (false) {
    // 重复声明一次函数f
    function f() { console.log('I am inside!'); }
  }

  f();
}());

上面代码在 ES5 中运行,会得到“I am inside!”,因为在if内声明的函数f会被提升到函数头部,实际运行的代码如下。

// ES5 环境
function f() { console.log('I am outside!'); }

(function () {
  function f() { console.log('I am inside!'); }
  if (false) {
  }
  f();
}());

ES6 就完全不一样了,理论上会得到“I am outside!”。因为块级作用域内声明的函数类似于let,对作用域之外没有影响。但是,如果你真的在 ES6 浏览器中运行一下上面的代码,是会报错的

// 浏览器的 ES6 环境
function f() { console.log('I am outside!'); }

(function () {
  if (false) {
    // 重复声明一次函数f
    function f() { console.log('I am inside!'); }
  }

  f();
}());
// Uncaught TypeError: f is not a function

如果改变了块级作用域内声明的函数的处理规则,显然会对老代码产生很大影响。为了减轻因此产生的不兼容问题,ES6 在附录 B里面规定,浏览器的实现可以不遵守上面的规定,有自己的行为方式

  • 允许在块级作用域内声明函数。

  • 函数声明类似于var,即会提升到全局作用域或函数作用域的头部。

  • 同时,函数声明还会提升到所在的块级作用域的头部。

注意,上面三条规则只对 ES6 的浏览器实现有效,其他环境的实现不用遵守,还是将块级作用域的函数声明当作let处理。

根据这三条规则,浏览器的 ES6 环境中,块级作用域内声明的函数,行为类似于var声明的变量。上面的例子实际运行的代码如下。

// 浏览器的 ES6 环境
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

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

// 块级作用域内部的函数声明语句,建议不要使用
{
  let a = 'secret';
  function f() {
    return a;
  }
}

// 块级作用域内部,优先使用函数表达式
{
  let a = 'secret';
  let f = function () {
    return a;
  };
}

ES6 的块级作用域必须有大括号,如果没有大括号,JavaScript 引擎就认为不存在块级作用域。

const 命令

const声明一个只读的常量。一旦声明,常量的值就不能改变。

const PI = 3.1415;
PI // 3.1415

PI = 3;
// TypeError: Assignment to constant variable.

const声明的变量不得改变值,这意味着,const一旦声明变量,就必须立即初始化,不能留到以后赋值。

const foo;
// SyntaxError: Missing initializer in const declaration

对于const来说,只声明不赋值,就会报错。

const的作用域与let命令相同:只在声明所在的块级作用域内有效。

if (true) {
  const MAX = 5;
}

MAX // Uncaught ReferenceError: MAX is not defined

const本质

const实际上保证的,并不是变量的值不得改动,而是变量指向的那个内存地址所保存的数据不得改动。对于简单类型的数据(数值、字符串、布尔值),值就保存在变量指向的那个内存地址,因此等同于常量。但对于复合类型的数据(主要是对象和数组),变量指向的内存地址,保存的只是一个指向实际数据的指针,const只能保证这个指针是固定的(即总是指向另一个固定的地址),至于它指向的数据结构是不是可变的,就完全不能控制了。因此,将一个对象声明为常量必须非常小心。

const foo = {};

// 为 foo 添加一个属性,可以成功
foo.prop = 123;
foo.prop // 123

// 将 foo 指向另一个对象,就会报错
foo = {}; // TypeError: "foo" is read-only

常量foo储存的是一个地址,这个地址指向一个对象。不可变的只是这个地址,即不能把foo指向另一个地址,但对象本身是可变的,所以依然可以为其添加新属性。

const a = [];
a.push('Hello'); // 可执行
a.length = 0;    // 可执行
a = ['Dave'];    // 报错

常量a是一个数组,这个数组本身是可写的,但是如果将另一个数组赋值给a,就会报错。

ES6 声明变量的六种方法

ES5 只有两种声明变量的方法:var命令和function命令。ES6 除了添加letconst命令,后面章节还会提到,另外两种声明变量的方法:import命令和class命令。所以,ES6 一共有 6 种声明变量的方法。

顶层对象的属性

顶层对象,在浏览器环境指的是window对象,在 Node 指的是global对象。ES5 之中,顶层对象的属性与全局变量是等价的。

window.a = 1;
a // 1

a = 2;
window.a // 2

顶层对象的属性是到处可以读写的,这非常不利于模块化编程。另一方面,window对象有实体含义,指的是浏览器的窗口对象,顶层对象是一个有实体含义的对象,也是不合适的。

为了保持兼容性,var命令和function命令声明的全局变量,依旧是顶层对象的属性;另一方面规定,let命令、const命令、class命令声明的全局变量,不属于顶层对象的属性。也就是说,从 ES6 开始,全局变量将逐步与顶层对象的属性脱钩。

var a = 1;
// 如果在 Node 的 REPL 环境,可以写成 global.a
// 或者采用通用方法,写成 this.a
window.a // 1

let b = 1;
window.b // undefined

全局变量avar命令声明,所以它是顶层对象的属性;全局变量blet命令声明,所以它不是顶层对象的属性,返回undefined

globalThis 对象

JavaScript 语言存在一个顶层对象,它提供全局环境(即全局作用域),所有代码都是在这个环境中运行。但是,顶层对象在各种实现里面是不统一的。

  • 浏览器里面,顶层对象是window,但 Node 和 Web Worker 没有window

  • 浏览器和 Web Worker 里面,self也指向顶层对象,但是 Node 没有self

  • Node 里面,顶层对象是global,但其他环境都不支持。

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

  • 全局环境中,this会返回顶层对象。但是,Node.js 模块中this返回的是当前模块,ES6 模块中this返回的是undefined

  • 函数里面的this,如果函数不是作为对象的方法运行,而是单纯作为函数运行,this会指向顶层对象。但是,严格模式下,这时this会返回undefined

  • 不管是严格模式,还是普通模式,new Function('return this')(),总是会返回全局对象。但是,如果浏览器用了 CSP(Content Security Policy,内容安全策略),那么evalnew Function这些方法都可能无法使用。

// 方法一
(typeof window !== 'undefined'
   ? window
   : (typeof process === 'object' &&
      typeof require === 'function' &&
      typeof global === 'object')
     ? global
     : this);

// 方法二
var getGlobal = function () {
  if (typeof self !== 'undefined') { return self; }
  if (typeof window !== 'undefined') { return window; }
  if (typeof global !== 'undefined') { return global; }
  throw new Error('unable to locate global object');
};

ES2020 在语言标准的层面,引入globalThis作为顶层对象。也就是说,任何环境下,globalThis都是存在的,都可以从它拿到顶层对象,指向全局环境下的this

变量的解构赋值

数组的解构赋值

基本用法

ES6允许按照一定模式,从数组和对象中提取值,对变量进行赋值,这就是解构。

let [a,b,c]=[1,2,3]

如果解构失败,变量的值为undefined

let [a]=[]//undefined
let [a,b]=[1]//a:1 b:undefined

默认值

解构赋值允许指定默认值

let [a=1]=[];
a //1

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

只有当一个数组成员严格等于undefined,默认值才会生效。

对象的解构赋值

解构不仅可以用于数组,还可以用于对象。

let{foo,bar}={foo:'aaa',bar:'bbb'}
foo//'aaa'
bar//'bbb'

对象的解构与数组的不同点,数组元素是按照次序排列的,变量的取值由它的位置决定。而对象的属性没有次序,变量必须与属性同名,才能取到真正的值。

let {bar,foo}={foo:'aaa',bar:'bbb'}
bar//'bbb'
foo//'aaa'

let {bao}={foo:'aaa',bar:'bbb'}
bao//undefined

与数组一样,解构也可以用于嵌套解构的对象。

let obj={
    a:[
        'hi',
        {name:'张三'}
    ]
};
let {a:[b,{name}]}=obj;
b//'hi'
name//'张三'

这是a是模式,不是变量,因此不会被赋值。如果a也要作为变量赋值,可以这样写如下

let obj={
    a:[
        'hi',
        {name:'张三'}
    ]
};
let {a,a:[b,{name}]}=obj
a//['hi',{name:'张三'}]
b//'hi'
name//'张三'

默认值

对象解构也可以指定默认值

let {x=3}={}
x//3

默认值生效的条件是,对象的属性严格等于undefined

let {x=3}={x:undefined}
x//3

let {x=5={x:null}
 x//null

属性x等于null,因为null和undefined不严格相等,所有是个有效值,导致默认值5不生效。

注意点

(1)如果要将一个已经声明的变量用于解构赋值,必须非常小心

//错误写法
let x;
{x}={x:1};
// SyntaxError: syntax error

因为javaScript引擎会将{x}理解为一个代码块,从而语法发生错误。只有不将大括号写在首行,避免javaScript将其解释为代码块,才能解决这个问题

let x;
({x}={x:1});

将整个解构赋值语句,放在一个圆括号里面,就可以正确执行。

(2)解构赋值允许等会左边的模式之中,不放置任何变量

({}=[true,false]);
({}='abc')
({}=[])

上述代码无任何意义,但不会报错

(3)由于数组本质是特殊对象,因此可以对数组进行对象属性的解构

let arr=[1,2,3]
let{0:frist,[arr.length-1]:last}=arr
frist//1
last//3

字符串的解构赋值

字符串也可以解构赋值,因为字符串会被转换为一个类似数组的对象

const[a,b,c,d,e]='hello'
a//h
b//e
c//l
d//l
e//o

let {length:len}='hi'
len//2

类似数组的对象都有一个length属性。

数值和布尔值的解构赋值

解构赋值时,如果等会右边是数值和布尔值,则会先转换为对象

let {toString:s}=123
s===Number.prototype.toString//true

let{toString:s}=true
s===Boolean.prototype.toString//true

数值和布尔值的包装对象都有toString属性,因此变量s都能取到值。

解构赋值的规则是,只要等会右边的值不是对象或数组,就先将其转换为对象,由于undefined和null无法转为对象,所以对他们解构赋值都会报错

let { prop: x } = undefined; // TypeError
let { prop: y } = null; // TypeError

函数参数的解构赋值

函数的参数也可以解构赋值,还可以使用默认值

function fn([x,y]){
    return x+y
}
fn([1,2])//3

function move({x=0,y=0}){
    return[x,y]
}
move({x:3,y:8})//[3,8]
move({x:3})//[3,0]
move({y:8})//[0,8]
move()//[0,0]

函数的参数表面上是一个数组,但在传入参数的那一刻,数组参数就被解构橙变量x和y。

圆括号问题

解构赋值虽然很方便,但是解析起来并不容易。对于编译器来说,一个式子到底是模式,还是表达式,没有办法从一开始就知道,必须解析到(或解析不到)等号才能知道。

由此带来的问题是,如果模式中出现圆括号怎么处理。ES6 的规则是,只要有可能导致解构的歧义,就不得使用圆括号。

但是,这条规则实际上不那么容易辨别,处理起来相当麻烦。因此,建议只要有可能,就不要在模式中放置圆括号。

不能使用圆括号的情况

(1)变量声明语句

let [(a)]=[1]
let ({x:1})={}
let {(x):1}={}
let {a:({p:p})}={o:{p:2}}

(2)函数参数

function fn([(z)]){return z}

function fn([z,(x)]){return}

(3)赋值语句的模式

({a:z})={a:55}
([a])=[5]

可以使用圆括号的情况

赋值语句的非模式部分,可以使用圆括号

[(b)]=[3]
({p:(a)}={})
[(parseInt.prop)]=[3]

上面三行语句都可以正确执行,因为首先它们都是赋值语句,而不是声明语句;其次它们的圆括号都不属于模式的一部分。第一行语句中,模式是取数组的第一个成员,跟圆括号无关;第二行语句中,模式是p,而不是d;第三行语句与第一行语句的性质一致。

用途

(1)交换变量

let x=1
let y=2

[x,y]=[y,x]

(2)从函数返回多个值

函数只能返回一个值,如果需要返回多个值,只能将它们放在数组或对象中返回。

//返回一个数组
function fn(){
    return [1,2,3]
}
let [a,b,c]=fn()


//返回一个对象
function fn(){
    return{
        foo:1,
        bar:2
    }
}
let{foo,bar}=fn()

(3)函数参数的定义

解构赋值可以方便的将一组参数与变量对应起来

//参数是一组有次序的值
function fn([x,y,z]){....}
fn([1,2,3])

//参数是一组无次序的值
function fn({x,y,z}){...}
fn({z:3,y:2,x:1})

(4)提取JSON数据

解构赋值对提取JSON对象的数据,尤其有用

let jsonData={
    id:18,
    name:"张三",
    data:[2000,10]
}
let{id,name,data}=jsonData

指定参数的默认值,就避免了在函数体内部再写var foo = config.foo || 'default foo';这样的语句。

(6)遍历Map解构

任何部署了 Iterator 接口的对象,都可以用for...of循环遍历。Map 结构原生支持 Iterator 接口,配合变量的解构赋值,获取键名和键值就非常方便。

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");

字符串扩展

字符串的Unicode表示法

允许采用\uxxxx形式表示一个字符,其中xxxx表示字符的 Unicode 码点。

"\u0061"
// "a"

这种表示法只限于码点在\u0000~\uFFFF之间的字符。超出这个范围的字符,必须用两个双字节的形式表示。

"\uD842\uDFB7"
// "𠮷"

"\u20BB7"
// " 7"

如果直接在\u后面跟上超过0xFFFF的数值(比如\u20BB7),JavaScript 会理解成\u20BB+7。由于\u20BB是一个不可打印字符,所以只会显示一个空格,后面跟着一个7

ES6 对这一点做出了改进,只要将码点放入大括号,就能正确解读该字符。

"\u{20BB7}"
// "𠮷"

"\u{41}\u{42}\u{43}"
// "ABC"

let hello = 123;
hell\u{6F} // 123

'\u{1F680}' === '\uD83D\uDE80'
// true 大括号表示法与四字节的 UTF-16 编码是等价的

有了这种表示法之后,JavaScript 共有 6 种方法可以表示一个字符。

'\z' === 'z'  // true
'\172' === 'z' // true
'\x7A' === 'z' // true
'\u007A' === 'z' // true
'\u{7A}' === 'z' // true

字符串的遍历器接口

ES6 为字符串添加了遍历器接口,使得字符串可以被for...of循环遍历。

for(let codePoint of 'foo'){
    console.log(codePoint)
}
//"f"
//"o"
//"o"

直接输入U+2028和U+2029

JavaScript 字符串允许直接输入字符,以及输入字符的转义形式。举例来说,“中”的 Unicode 码点是 U+4e2d,你可以直接在字符串里面输入这个汉字,也可以输入它的转义形式\u4e2d,两者是等价的。

"中" ==="\u4e2d" //true

JavaScript 规定有5个字符,不能在字符串里面直接使用,只能使用转义形式。

  • U+005C:反斜杠(reverse solidus)

  • U+000D:回车(carriage return)

  • U+2028:行分隔符(line separator)

  • U+2029:段分隔符(paragraph separator)

  • U+000A:换行符(line feed)

JSON.stringify()的改造

根据标准,JSON 数据必须是 UTF-8 编码。但是,现在的JSON.stringify()方法有可能返回不符合 UTF-8 标准的字符串。

UTF-8 标准规定,0xD8000xDFFF之间的码点,不能单独使用,必须配对使用。比如,\uD834\uDF06是两个码点,但是必须放在一起配对使用,代表字符𝌆。这是为了表示码点大于0xFFFF的字符的一种变通方法。单独使用\uD834\uDF06这两个码点是不合法的,或者颠倒顺序也不行,因为\uDF06\uD834并没有对应的字符。

为了确保返回的是合法的 UTF-8 字符,ES2019 改变了JSON.stringify()的行为。如果遇到0xD8000xDFFF之间的单个码点,或者不存在的配对形式,它会返回转义字符串,留给应用自己决定下一步的处理。

JSON.stringify('\u{D834}') // ""\\uD834""
JSON.stringify('\uDF06\uD834') // ""\\udf06\\ud834""

模板字符串

模板字符串(template string)是增强版的字符串,用反引号(`)标识。它可以当作普通字符串使用,也可以用来定义多行字符串,或者在字符串中嵌入变量。

// 普通字符串
`In JavaScript '\n' is a line-feed.`

// 多行字符串
`In JavaScript this is
 not legal.`

console.log(`string text line 1
string text line 2`);

// 字符串中嵌入变量
let name = "Bob", time = "today";
`Hello ${name}, how are you ${time}?`

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值