走进ES6的世界:基础语法入门指南1

简介: 走进ES6的世界:基础语法入门指南

1、ECMAScript 6 简介

ECMAScript 6.0(以下简称 ES6)是 JavaScript 语言的下一代标准,已经在 2015 年 6 月正式发布了。它的目标,是使得 JavaScript 语言可以用来编写复杂的大型应用程序,成为企业级开发语言。

1.1、ECMAScript 和 JavaScript 的关系

1996 年 11 月,JavaScript 的创造者 Netscape 公司,决定将 JavaScript 提交给标准化组织 ECMA,希望这种语言能够成为国际标准。次年,ECMA 发布 262 号标准文件(ECMA-262)的第一版,规定了浏览器脚本语言的标准,并将这种语言称为 ECMAScript,这个版本就是 1.0 版。

该标准从一开始就是针对 JavaScript 语言制定的,但是之所以不叫 JavaScript,有两个原因。一是商标,Java 是 Sun 公司的商标,根据授权协议,只有 Netscape 公司可以合法地使用 JavaScript 这个名字,且 JavaScript 本身也已经被 Netscape 公司注册为商标。二是想体现这门语言的制定者是 ECMA,不是 Netscape,这样有利于保证这门语言的开放性和中立性。

因此,ECMAScript 和 JavaScript 的关系是,前者是后者的规格,后者是前者的一种实现(另外的 ECMAScript 方言还有 JScript 和 ActionScript)。日常场合,这两个词是可以互换的。

1.2、ES6 与 ECMAScript 2015 的关系

ECMAScript 2015(简称 ES2015)这个词,也是经常可以看到的。它与 ES6 是什么关系呢?

2011 年,ECMAScript 5.1 版发布后,就开始制定 6.0 版了。因此,ES6 这个词的原意,就是指 JavaScript 语言的下一个版本。

但是,因为这个版本引入的语法功能太多,而且制定过程当中,还有很多组织和个人不断提交新功能。事情很快就变得清楚了,不可能在一个版本里面包括所有将要引入的功能。常规的做法是先发布 6.0 版,过一段时间再发 6.1 版,然后是 6.2 版、6.3 版等等。

但是,标准的制定者不想这样做。他们想让标准的升级成为常规流程:任何人在任何时候,都可以向标准委员会提交新语法的提案,然后标准委员会每个月开一次会,评估这些提案是否可以接受,需要哪些改进。如果经过多次会议以后,一个提案足够成熟了,就可以正式进入标准了。这就是说,标准的版本升级成为了一个不断滚动的流程,每个月都会有变动。

标准委员会最终决定,标准在每年的 6 月份正式发布一次,作为当年的正式版本。接下来的时间,就在这个版本的基础上做改动,直到下一年的 6 月份,草案就自然变成了新一年的版本。这样一来,就不需要以前的版本号了,只要用年份标记就可以了。

ES6 的第一个版本,就这样在 2015 年 6 月发布了,正式名称就是《ECMAScript 2015 标准》(简称 ES2015)。2016 年 6 月,小幅修订的《ECMAScript 2016 标准》(简称 ES2016)如期发布,这个版本可以看作是 ES6.1 版,因为两者的差异非常小(只新增了数组实例的includes方法和指数运算符),基本上是同一个标准。根据计划,2017 年 6 月发布 ES2017 标准

因此,ES6 既是一个历史名词,也是一个泛指,含义是 5.1 版以后的 JavaScript 的下一代标准,涵盖了 ES2015、ES2016、ES2017 等等,而 ES2015 则是正式名称,特指该年发布的正式版本的语言标准。本书中提到 ES6 的地方,一般是指 ES2015 标准,但有时也是泛指“下一代 JavaScript 语言”。

1.3、ECMAScript 的历史

ES6 从开始制定到最后发布,整整用了 15 年。

前面提到,ECMAScript 1.0 是 1997 年发布的,接下来的两年,连续发布了 ECMAScript 2.0(1998 年 6 月)和 ECMAScript 3.0(1999 年 12 月)。3.0 版是一个巨大的成功,在业界得到广泛支持,成为通行标准,奠定了 JavaScript 语言的基本语法,以后的版本完全继承。直到今天,初学者一开始学习 JavaScript,其实就是在学 3.0 版的语法。

2000 年,ECMAScript 4.0 开始酝酿。这个版本最后没有通过,但是它的大部分内容被 ES6 继承了。因此,ES6 制定的起点其实是 2000 年。

为什么 ES4 没有通过呢?因为这个版本太激进了,对 ES3 做了彻底升级,导致标准委员会的一些成员不愿意接受。ECMA 的第 39 号技术专家委员会(Technical Committee 39,简称 TC39)负责制订 ECMAScript 标准,成员包括 Microsoft、Mozilla、Google 等大公司。

2007 年 10 月,ECMAScript 4.0 版草案发布,本来预计次年 8 月发布正式版本。但是,各方对于是否通过这个标准,发生了严重分歧。以 Yahoo、Microsoft、Google 为首的大公司,反对 JavaScript 的大幅升级,主张小幅改动;以 JavaScript 创造者 Brendan Eich 为首的 Mozilla 公司,则坚持当前的草案。

2008 年 7 月,由于对于下一个版本应该包括哪些功能,各方分歧太大,争论过于激烈,ECMA 开会决定,中止 ECMAScript 4.0 的开发,将其中涉及现有功能改善的一小部分,发布为 ECMAScript 3.1,而将其他激进的设想扩大范围,放入以后的版本,由于会议的气氛,该版本的项目代号起名为 Harmony(和谐)。会后不久,ECMAScript 3.1 就改名为 ECMAScript 5。

2009 年 12 月,ECMAScript 5.0 版正式发布。Harmony 项目则一分为二,一些较为可行的设想定名为 JavaScript.next 继续开发,后来演变成 ECMAScript 6;一些不是很成熟的设想,则被视为 JavaScript.next.next,在更远的将来再考虑推出。TC39 委员会的总体考虑是,ES5 与 ES3 基本保持兼容,较大的语法修正和新功能加入,将由 JavaScript.next 完成。当时,JavaScript.next 指的是 ES6,第六版发布以后,就指 ES7。TC39 的判断是,ES5 会在 2013 年的年中成为 JavaScript 开发的主流标准,并在此后五年中一直保持这个位置。

2011 年 6 月,ECMAScript 5.1 版发布,并且成为 ISO 国际标准(ISO/IEC 16262:2011)。

2013 年 3 月,ECMAScript 6 草案冻结,不再添加新功能。新的功能设想将被放到 ECMAScript 7。

2013 年 12 月,ECMAScript 6 草案发布。然后是 12 个月的讨论期,听取各方反馈。

2015 年 6 月,ECMAScript 6 正式通过,成为国际标准。从 2000 年算起,这时已经过去了 15 年。

2、let 和 const 命令

2.1、let 命令

2.1.1、基本用法

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

{
  let a = 10;
  var b = 1;
}
console.log(a) // ReferenceError: a is not defined.
console.log(b) // 1

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

2.1.2、不存在变量提升

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

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

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

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

2.1.3、不允许重复声明

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() // 不报错

2.1.4、暂时性死区

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

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

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

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

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

2.2、const 命令

2.2.1、基本用法

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命令声明的常量也是不提升,同样存在暂时性死区,只能在声明的位置后面使用。

if (true) {
  console.log(MAX); // ReferenceError
  const MAX = 5;
}

上面代码在常量MAX声明之前就调用,结果报错。

const声明的常量,也与let一样不可重复声明。

var message = "Hello!";
let age = 25;
// 以下两行都会报错
const message = "Goodbye!";
const age = 30;

2.2.2、本质

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,就会报错。

3、变量的解构赋值

3.1、数组的解构赋值

3.1.1、基本用法

以前,为变量赋值,只能直接指定值。

let a = 1;
let b = 2;
let c = 3;

ES6 允许写成下面这样。

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

上面代码表示,可以从数组中提取值,按照对应位置,对变量赋值。

本质上,这种写法属于“模式匹配”,只要等号两边的模式相同,左边的变量就会被赋予对应的值。下面是一些使用嵌套数组进行解构的例子。

let [foo, [[bar], baz]] = [1, [[2], 3]];
foo // 1
bar // 2
baz // 3
let [x, , y] = [1, 2, 3];
x // 1
y // 3

如果解构不成功,变量的值就等于undefined。

let [foo] = [];
let [bar, foo] = [1];

以上两种情况都属于解构不成功,foo的值都会等于undefined。

3.1.2、默认值

解构赋值允许指定默认值。

let [foo = true] = [];
foo // true
let [x, y = 'b'] = ['a']; // x='a', y='b'
let [x, y = 'b'] = ['a', undefined]; // x='a', y='b'

注意,ES6 内部使用严格相等运算符(===),判断一个位置是否有值。所以,只有当一个数组成员严格等于undefined,默认值才会生效。

let [x = 1] = [undefined];
x // 1
let [x = 1] = [null];
x // null

上面代码中,如果一个数组成员是null,默认值就不会生效,因为null不严格等于undefined。

3.2、对象的解构赋值

3.2.1、基本用法

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

let { bar, foo } = { foo: 'aaa', bar: 'bbb' };
foo // "aaa"
bar // "bbb"
let { baz } = { foo: 'aaa', bar: 'bbb' };
baz // undefined    如果解构失败,变量的值等于`undefined`

上面代码的第一个例子,等号左边的两个变量的次序,与等号右边两个同名属性的次序不一致,但是对取值完全没有影响。第二个例子的变量没有对应的同名属性,导致取不到值,最后等于undefined。

如果变量名与属性名不一致,必须写成下面这样。

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'

这实际上说明,对象的解构赋值是下面形式的简写。

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

也就是说,对象的解构赋值的内部机制,是先找到同名属性,然后再赋给对应的变量。真正被赋值的是后者,而不是前者。

let { foo: baz } = { foo: 'aaa', bar: 'bbb' };
baz // "aaa"
foo // error: foo is not defined

上面代码中,foo是匹配的模式,baz才是变量。真正被赋值的是变量baz,而不是模式foo。

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

let obj = {
  p: [
    'Hello',
    { y: 'World' }
  ]
};
let { p: [x, { y }] } = obj;
x // "Hello"
y // "World"

注意,这时p是模式,不是变量,因此不会被赋值。如果p也要作为变量赋值,可以写成下面这样。

let obj = {
  p: [
    'Hello',
    { y: 'World' }
  ]
};
let { p } = obj;
p // ["Hello", {y: "World"}]

3.2.2、默认值

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

var {x = 3} = {};
x // 3
var {x, y = 5} = {x: 1};
x // 1
y // 5

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

var {x = 3} = {x: undefined};
x // 3
var {x = 3} = {x: null};
x // null

上面代码中,属性x等于null,因为null与undefined不严格相等,所以是个有效的赋值,导致默认值3不会生效。

3.2.3、注意点

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

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

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

// 正确的写法
let x;
({x} = {x: 1});

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

3.2.4、项目中的使用场景

1、对this进行解构,减少this的滥用

//在computed中对this进行解构
computed: {
    btnsArr({isShowDispatch, isPaperWorkTicket, wtPaperValidOrg}) {
  // 派工单或者是四川个性化纸质票不展示保存按钮
  return isShowDispatch || (isPaperWorkTicket && wtPaperValidOrg) ? ['提交'] :['保  存', '提交']
  }
}
//在methods中对this进行解构
methods: {
    getData(){
       let { getIssuerArr, ticketCodeValue } = this
       /*
       *getIssuerArr.forEach(item=>{
       *   ...//处理数据的逻辑
       *}) 
       */     
    }
}

2、在通过接口拿到后端获取的数据,进行处理的时候,常用对象的解构赋值代替普通的赋值操作来优化代码。

//获取接口返回的数据res
const res = await workTicket.getSignerList(params)
if (res.RT_F === 1) {
    //对返回值进行解构,赋值给对应的变量
  let { signerList, defaultSigner, defaultSignerVo } = res.DTS || {}
  ...//后续的逻辑处理
}

3.3、字符串的解构赋值

字符串也可以解构赋值。这是因为此时,字符串被转换成了一个类似数组的对象。(解构赋值的规则是,只要等号右边的值不是对象或数组,就先将其转为对象。)

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Xmo0VgC3-1680140295224)(ES6基础知识.assets/image-20220728095627004.png)]

类似数组的对象都有一个length属性,因此还可以对这个属性解构赋值。

let {length : len} = 'hello';
len // 5

3.4、函数参数的解构赋值

函数的参数也可以使用解构赋值。

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

上面代码中,函数add的参数表面上是一个数组,但在传入参数的那一刻,数组参数就被解构成变量x和y。对于函数内部的代码来说,它们能感受到的参数就是x和y。

函数参数的解构也可以使用默认值。

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

上面代码中,函数move的参数是一个对象,通过对这个对象进行解构,得到变量x和y的值。如果解构失败,x和y等于默认值。

注意,下面的写法会得到不一样的结果。

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

上面代码是为函数move的参数指定默认值,而不是为变量x和y指定默认值,所以会得到与前一种写法不同的结果。

4、运算符的扩展

4.1、扩展运算符和剩余参数

扩展运算符(Spread Operator)和剩余参数(Rest Parameter)的写法相同,都是在变量或字面量之前加三个点(…)。虽然两者之间有诸多类似,但它们的功能和应用场景却完全不同。扩展运算符能把整体展开成个体,常用于函数调用、数组或字符串处理等;而剩余参数正好相反,把个体合并成整体,常用于函数声明、解构参数等。此处的整体可能是数组、字符串或类数组对象等,个体可能是字符、数组的元素或函数的参数等。

4.1.1、扩展运算符

扩展运算符的用途简单概括,可以分为以下三种。

 (1)替代函数的apply()方法。

 (2)简化函数调用时传递实参的方式。

 (3)处理数组和字符串。

1)apply()

 函数的apply()方法能够间接调用其它对象的方法,往往能收获奇效,例如用Math对象的min()方法获取数组中的最小值,min()方法本来只接收一组参数,利用apply()方法后就能直接传递一个数组,如下所示。

let arr = [1, 0, 2],
  min;
min = Math.min(1, 0, 2);                 //一组参数的调用方式
min = Math.min.apply(null, arr);    //利用apply()间接调用

虽然apply()方法很便捷,但每次都必须设置this的指向(即定义第一个参数),并且迂回的写法可能会为理解代码意图设置障碍。而使用扩展运算符后,既能以简单的语法形式完成相同的功能,还能更清晰的表明代码的意图。下面用扩展运算符查找数组中的最小值。

min = Math.min(...arr);
console.log(min);     //0

2)传参

函数在被调用时,实参通常都是以逗号分隔的序列形式传递到函数体内。如果实参的值被保存在数组中,那么就要一个一个的读取数组中指定位置的元素,例如创建一个日期对象(调用它的构造函数),把年月日的信息保存在数组中,如下代码所示。注释中的日期并不是默认的显示格式,只是为了更容易阅读而这么写的。

let date = [2018, 7, 6];
new Date(date[0], date[1], date[2]);    //2018-7-6

换成扩展运算符的写法后,实参的传递就变得非常的简洁,如下所示。

new Date(...date);                      //2018-7-6

不仅如此,在调用函数的时候,还可以使用多个扩展运算符,并能和普通的实参混合使用,如下所示。

let time = [10, 28];
new Date(...date, ...time, 45);         //2018-7-6 10:28:45

3)数组和字符串

在扩展运算符出现之前,要执行数组的复制、合并等操作,需要调用数组的slice()、concat()、unshift()等方法。下面是一个数组复制与合并的简单示例。

let arr1 = [1, 2, 3],
  arr2,
  arr3;
arr2 = arr1.slice();       //复制数组
arr3 = arr2.concat(arr1);  //合并数组
console.log(arr1);         //[1, 2, 3]
console.log(arr2);         //[1, 2, 3]
console.log(arr3);         //[1, 2, 3, 1, 2, 3]

接下来用扩展运算符来完成同样的功能,如下代码所示。

arr2 = [...arr1];              //复制数组
arr3 = [...arr1, ...arr2];     //合并数组

2在实际项目中,肯定会碰到各式各样的数组操作,合理利用扩展运算符,不但可以节省大量的代码,还能提升代码的可读性。

扩展运算符不仅能处理数组,还能处理字符串。在JavaScript中,字符串的行为类似于数组,但它不能直接调用数组的方法,需要先执行自己的split()方法转换成数组。而使用扩展运算符后,就能省去这步操作,具体如下所示,注意,包裹的方括号不能省略。

let str = "strick";
str.split("");        //["s", "t", "r", "i", "c", "k"]
[...str];             //["s", "t", "r", "i", "c", "k"]

4.1.2、剩余参数

在JavaScript的函数中,声明时定义的形参个数可以和传入的实参个数不同。当实参个数大于形参个数时,ES6新增的剩余参数能把没有对应形参的实参收集到一个数组中。下面是一个简单的示例。

function func(name, ...args) {
  console.log(name);
  console.log(args[0]);
}
args = [29,'88']
func("strick");         //首先输出"strick",然后输出undefined
func("freedom", 29,'88');    //首先输出"freedom",然后输出29

第一次调用func()函数只传入了一个实参,对应的形参就是name。第二次调用func()函数传入了两个实参,第一个有对应的形参,而第二个并没有对应的形参。此时,该实参就会被放到数组args(就是剩余参数)中,变为该数组的一个元素,在函数体内就能通过数组的索引读取该实参。有一点要注意,剩余参数不会影响函数的length属性,该属性的值表示形参个数。以上面的func()函数为例,… args并不是一个形参,因此,func()函数的length属性值为1。

console.log(func.length);         //1

剩余参数 之前,我们在接触多个参数的时候,通常使用arguments对象去代替接收多个参数。但是,arguments是类数组对象,不能使用数组的很多便捷操作, 箭头函数当中没有arguments对象 。

下面是一个 rest 参数代替arguments变量的例子。

// arguments变量的写法
function sortNumbers() {
  return Array.from(arguments).sort();
}
// rest参数的写法
const sortNumbers = (...numbers) => numbers.sort();

1) 两点限制

剩余参数有两点限制,在使用时需要引起注意。第一点是在函数中声明时必须放在最后,下面是一种错误的写法。

function restrict1(...args, name) {
  //抛出语法错误
}

第二点是不能在对象字面量的setter方法中声明,因为该方法只接收一个参数,而剩余参数不会限制参数的数量。注意,setter方法在定义时会用set替代function关键字,下面是一个会抛出语法错误的例子。

var obj = {
  set age(...value) {
    this._age = value;
  }
};

4.2、链判断运算符

编程实务中,如果读取对象内部的某个属性,往往需要判断一下,属性的上层对象是否存在。比如,读取message.body.user.firstName这个属性,安全的写法是写成下面这样。

// 错误的写法
const  firstName = message.body.user.firstName || 'default';
// 正确的写法
const firstName = (message
  && message.body
  && message.body.user
  && message.body.user.firstName) || 'default';

上面例子中,firstName属性在对象的第四层,所以需要判断四次,每一层是否有值。

这样的层层判断非常麻烦,因此 ES2020 引入了“链判断运算符”(optional chaining operator)?.,简化上面的写法。

const firstName = message?.body?.user?.firstName || 'default';

上面代码使用了?.运算符,直接在链式调用的时候判断,左侧的对象是否为null或undefined。如果是的,就不再往下运算,而是返回undefined。

4.3、Null 判断运算符

读取对象属性的时候,如果某个属性的值是null或undefined,有时候需要为它们指定默认值。常见做法是通过||运算符指定默认值。

const headerText = response.settings.headerText || 'Hello, world!';
const animationDuration = response.settings.animationDuration || 300;
const showSplashScreen = response.settings.showSplashScreen || true;

上面的三行代码都通过||运算符指定默认值,但是这样写是错的。开发者的原意是,只要属性的值为null或undefined,默认值就会生效,但是属性的值如果为空字符串或false或0,默认值也会生效。

为了避免这种情况,ES2020 引入了一个新的 Null 判断运算符??。它的行为类似||,但是只有运算符左侧的值为null或undefined时,才会返回右侧的值。

const headerText = response.settings.headerText ?? 'Hello, world!';
const animationDuration = response.settings.animationDuration ?? 300;
const showSplashScreen = response.settings.showSplashScreen ?? true;

上面代码中,默认值只有在左侧属性值为null或undefined时,才会生效。

这个运算符的一个目的,就是跟链判断运算符?.配合使用,为null或undefined的值设置默认值。

const animationDuration = response.settings?.animationDuration ?? 300;

4.4、项目中的使用过场景

读取层级很深的对象:

//在computed获取配置信息
computed: {
    isFillPreview({sysParasInfo}) {
  return sysParasInfo?.WT_PREVIEW_RULE?.includes('01')
  },
    getPreviewPage({sysParasInfo}) {
  return sysParasInfo?.WT_TICKET_PRE_STYLE ?? '01'
  },  
}

5、字符串的扩展

5.1、模板字符串

ES6 引入了模板字符串,用反引号表示,模板字符串中嵌入变量,需要将变量名写在${}之中。

//用uni.redirectTo进行页面跳转时url使用模板字符串
uni.redirectTo({
  url: `/pages/work-plan/work-week-detail?planId=${res.DTS}&openFrom=${this.openFrom}`
})

如果使用模板字符串表示多行字符串,所有的空格和缩进都会被保留在输出之中

5.2、字符串的新增方法

(1) 实例方法:includes(), startsWith(), endsWith()

传统上,JavaScript 只有indexOf方法,可以用来确定一个字符串是否包含在另一个字符串中。ES6 又提供了三种新方法。

  • includes():返回布尔值,表示是否找到了参数字符串。
  • startsWith():返回布尔值,表示参数字符串是否在原字符串的头部。
  • endsWith():返回布尔值,表示参数字符串是否在原字符串的尾部。
let s = 'Hello world!';
s.startsWith('Hello') // true
s.endsWith('!') // true
s.includes('o') // true

这三个方法都支持第二个参数,表示开始搜索的位置。

let s = 'Hello world!';
s.startsWith('world', 6) // true
s.endsWith('Hello', 5) // true
s.includes('Hello', 6) // false

上面代码表示,使用第二个参数n时,endsWith的行为与其他两个方法有所不同。它针对前n个字符,而其他两个方法针对从第n个位置直到字符串结束。

项目中的使用场景

//判断SYS_IGW_FUNC对应的值(字符串类型)中是否包含02
计算:{
isShowIndexAndCloseBtn() {
const sysParasInfo = this.$store.state.sysParasInfo
返回系统ParasInfo?。SYS_IGW_FUNC?。包括('02')
},
}

(2) 实例方法:padStart(),padEnd()

ES2017 引入了字符串补全长度的功能。如果某个字符串不够指定长度,会在头部或尾部补全。padStart()用于头部补全,padEnd()用于尾部补全。

'x'.padStart(5, 'ab') // 'ababax'
'x'.padEnd(5, 'ab') // 'xabab'

上面代码中,padStart()和padEnd()一共接受两个参数,第一个参数是字符串补全生效的最大长度,第二个参数是用来补全的字符串。

如果原字符串的长度,等于或大于最大长度,则字符串补全不生效,返回原字符串。

'xxx'.padStart(2, 'ab') // 'xxx'
'xxx'.padEnd(2, 'ab') // 'xxx'

如果用来补全的字符串与原字符串,两者的长度之和超过了最大长度,则会截去超出位数的补全字符串。

'abc'.padStart(10, '0123456789')
// '0123456abc'

如果省略第二个参数,默认使用空格补全长度。

'x'.padStart(4) // '   x'
'x'.padEnd(4) // 'x   '

padStart()的常见用途是为数值补全指定位数。下面代码生成 10 位的数值字符串。

'1'.padStart(10, '0') // "0000000001"
'12'.padStart(10, '0') // "0000000012"
'123456'.padStart(10, '0') // "0000123456"

另一个用途是提示字符串格式。

'12'.padStart(10, 'YYYY-MM-DD') // "YYYY-MM-12"
'09-12'.padStart(10, 'YYYY-MM-DD') // "YYYY-09-12"

(3) 实例方法:trimStart(),trimEnd()

ES2019 对字符串实例新增了trimStart()和trimEnd()这两个方法。它们的行为与trim()一致,trimStart()消除字符串头部的空格,trimEnd()消除尾部的空格。它们返回的都是新字符串,不会修改原始字符串。

const s = '  abc  ';
s.trim() // "abc"
s.trimStart() // "abc  "
s.trimEnd() // "  abc"

上面代码中,trimStart()只消除头部的空格,保留尾部的空格。trimEnd()也是类似行为。

除了空格键,这两个方法对字符串头部(或尾部)的 tab 键、换行符等不可见的空白符号也有效。

(4) 实例方法:replaceAll()

历史上,字符串的实例方法replace()只能替换第一个匹配。

'aabbcc'.replace('b', '_')
// 'aa_bcc'

上面例子中,replace()只将第一个b替换成了下划线。

如果要替换所有的匹配,不得不使用正则表达式的g修饰符。

'aabbcc'.replace(/b/g, '_')
// 'aa__cc'

正则表达式毕竟不是那么方便和直观,ES2021 引入了replaceAll()方法,可以一次性替换所有匹配。

'aabbcc'.replaceAll('b', '_')
// 'aa__cc'

它的用法与replace()相同,返回一个新字符串,不会改变原字符串。



目录
相关文章
从零开始学习 Java:简单易懂的入门指南之正则表达式(十五)
从零开始学习 Java:简单易懂的入门指南之正则表达式(十五)
|
1月前
|
机器学习/深度学习 数据可视化 数据挖掘
探索Python之美:从基础到进阶的编程之旅
【10月更文挑战第40天】在编程的世界里,Python以其简洁明了的语法和强大的功能库赢得了无数开发者的青睐。本篇文章将带你领略Python的魅力,从基础的语法入门到高级特性的应用,我们一同踏上这段充满智慧与挑战的编程之旅。
|
7月前
|
Java 编译器 Go
【字节跳动青训营】后端笔记整理-1 | Go语言入门指南:基础语法和常用特性解析(一)
本文主要梳理自第六届字节跳动青训营(后端组)-Go语言原理与实践第一节(王克纯老师主讲)。
200 1
|
6月前
|
自然语言处理 编译器 C语言
C++语言入门教程
C++语言入门教程
|
7月前
|
存储 Go C++
【字节跳动青训营】后端笔记整理-1 | Go语言入门指南:基础语法和常用特性解析(二)
Go 语言中的复合数据类型包括数组、切片(slice)、映射(map)和结构体(struct)。
83 0
|
7月前
|
存储 JSON Java
【字节跳动青训营】后端笔记整理-1 | Go语言入门指南:基础语法和常用特性解析(三)
在 Go 语言里,符合语言习惯的做法是使用一个单独的返回值来传递错误信息。
100 0
|
前端开发
走进ES6的世界:基础语法入门指南4
走进ES6的世界:基础语法入门指南
64 0
|
7月前
|
存储 程序员 开发工具
Python 进阶指南(编程轻松进阶):十四、实践项目
Python 进阶指南(编程轻松进阶):十四、实践项目
57 0
|
Go 开发者
长文回顾Go语言基础语法 新手进阶必读
长文回顾Go语言基础语法 新手进阶必读
51 0
长文回顾Go语言基础语法 新手进阶必读
|
存储 Java 关系型数据库
No1.入门第一课,我的go开发之旅
No1.入门第一课,我的go开发之旅