Web学习笔记——ES6(上)


ES6是 2015 年发布的一个 JavaScript 版本,被命名为 ECMAScript 2015,也叫 ECMAScript 6,故简称为 ES6。

一、新增关键字

1、var 关键字的缺点

变量提升机制的问题
function getNumber(isNumber) {
  if (isNumber) {
    var num = "7";
  } else {
    var notNum = "not number!";
    console.log(num);
  }
}
getNumber(false);

在控制台可以看到报出“undefined”的错。
在 JavaScript 中有一个提升机制,就是无论你在哪里使用 var 关键字声明变量,它都会被提升到当前作用域的顶部。在运行 getNumber 函数时,实际上执行结构是下面这个样子。

function getNumber(isNumber) {
  var num;
  var notNum;
  if (isNumber) {
    num = "7";
  } else {
    notNum = "not number!";
    console.log(num);
  }
}
getNumber(false);

因为在开头定义变量时,没有给 num 变量赋任何值,并且 getNumber 传入的是 false,导致 if 语句未执行,num 未被赋值,所以控制台输出 undefined。

变量重复声明的问题
function Sum(arrList) {
  var sum = 0;
  for (var i = 0; i < arrList.length; i++) {
    var arr = arrList[i];
    for (var i = 0; i < arr.length; i++) {
      sum += arr[i];
    }
  }
  return sum;
}
var arr = [1, 2, 3, 4, 5];
document.write(Sum(arr));

在两层 for 循环中我们使用同一变量 i 进行赋值时,代码在执行过程中,第二层的 for 循环会覆盖外层变量 i 的值。

非块作用域的问题

使用 var 关键字定义的变量只有两种作用域,全局作用域和函数作用域,两者均不是块结构,会造成变量声明的提升。这可能出现下面这种问题:

function func() {
  for (var i = 0; i < 5; i++) {}
  document.write(i); // 5
}
func();

运行上述代码后,会发现页面上会显示 5。我们虽然是在 for 循环中定义的 i 变量,但由于变量被提升到 for 语句之上,所以退出循环后,变量 i 并没有被销毁,我们能够在循环外获取它的值。

2、let 关键字

解决变量提升机制问题

ES6 为我们提供了 let 关键字,它解决了变量提升到作用域顶部的问题。因为它的作用域是块,而不是提升机制了。

function getNumber(isNumber) {
  if (isNumber) {
    let num = "7";
  } else {
    let notNum = "not number!";
    console.log(num);
  }
}
getNumber(false);

控制台会报出ReferenceError的错,ReferenceError 是一个引用类型的错误,num is not defined 意思是 num 变量并不存在。
因为 let 关键字声明变量,其作用域是一个块,如果我们是在花括号 {} 里面声明变量,那么变量会陷入暂时性死区,也就是在声明之前,变量不可以被使用。
在上面代码中,我们的 num 变量是放在 if(){} 这个块中,并没有在 else{} 块中,所以会形成暂时性死区。
在这里插入图片描述

解决变量重复声明的问题

虽然 let 关键字声明的变量可以重新赋值,但是它与 var 关键字有所不同,let 关键字不能在同一作用域内重新声明,而 var 可以。

let i = 5;
let i = 6;
console.log(i);//控制台报参数错误(SyntaxError)

var i = 5;
var i = 6;
console.log(i);//不会报错
解决非块级作用域的问题

let 关键字定义的变量是块级作用域,避免了变量提升。

function func() {
  for (let i = 0; i < 5; i++) {}
  console.log(i);
}
func();

控制台会报出ReferenceError的错。因为上面代码中的 i 变量只存在于 for 循环这个块中,当循环结束,i 变量就被销毁了,所以在 for 循环外访问不到。

3、const 关键字

在 ES6 中,为我们提供了另一个关键字 const 用于声明一个只读的常量。且一旦声明,常量的值就不能改变。如果你尝试反复赋值的话,则会引发错误。例如:

const MaxAge = 100;
MaxAge = 10;
console.log(MaxAge);

既然是不可改变,那么我们在定义时,必须对它进行初始化,不然也会报错。例如:

const Num;
console.log(Num);

对于 const 关键字定义的变量值,不可改变在于两个方面:
1. 值类型
值类型是指变量直接存储的数据,例如:

const num = 20;

这里 num 变量就是值类型,我们使用的是 const 关键字来定义 num,故赋予变量 num 的值 20 是不可改变的。
2. 引用类型
引用类型是指变量存储数据的引用,而数据是放在数据堆中,比如,用 const 声明一个数组。

const arr = ["一", "二", "三"];

如果你尝试去修改数组,同样会报错。

const arr = ["一", "二", "三"];
arr = ["五", "六", "七"];

但是,使用 const 关键字定义的引用类型还是可以通过数组下标去修改值 。
例如:

const arr = ["一", "二", "三"];
arr[0] = "四";
arr[1] = "五";
arr[2] = "六";
console.log(arr);

因为变量 arr 保存的是数组的引用,并不是数组中的值,只要引用的地址不发生改变就不会保错。这就相当于一个房子,它拥有固定的位置,但住在房子里的人不一定固定。
回顾一下 const、let、var 三者之间的区别:

  • var 语句的作用域是函数作用域或者全局作用域;它没有块作用域,故不存在暂时死区;它可分配,也可重复性声明。
  • let 语句属于块作用域,受到暂时死区的约束;它可分配,但不可重新声明。
  • const 语句也属于块作用域,同样受到暂时死区的约束;它既不可重新分配,也不可重新声明。

二、字符串的扩展

1、模板字面量

模板这个词很常见,指的是可以复用的东西。在 JavaScript 中,字面量代表由一些字符组成表达式定义的常量。

模板字面量的基础语法

在 ES6 中提供了反撇号去代替单引号和双引号。

let str = `Hello LanQiao!`;
console.log("输出字符串:" + str);
console.log("字符串类型:" + typeof str);
console.log("字符串长度:" + str.length);

当使用模板字符串创建了一个名为 str 的字符串,在控制台输出了字符串、字符串的类型、字符串的长度,可以发现,这和以前使用单引号或者双引号,创建字符串得到的结果是一模一样。

多行字符串的处理
let str = `Hello,
ECMAScript 6`;
      console.log(str);

模板字面量有个特点,定义在反撇号中的字符串,其中的空格、缩紧、换行都会被保留。

字符串占位符

在 JavaScript 中,占位符由 ${} 符号组成,在花括号的中间可以包含任意 JavaScript 表达式。

let str = `LanQiao Courses`;
let message = `I like ${str}.`;
console.log(message);
let a = 2;
let b = 1;
let sum = `a+b=${a + b}`;
console.log(sum);
标签模板

这里的标签并不是在 HTML 中所说的标签,这里的标签相当于是一个函数。而标签模板就是执行模板字面量上的转换并返回最终的字符串。

let name = `JavaScript`;
let str = tag`Welcome to ${name} course.`;

在上面的代码中,用于模板字面量的模板标签就是 tag 。(注意:上面的 tag 并不是系统自带的,而是需要我们去定义的函数。)
这里的标签,我们可以理解为函数,该函数的参数说明如下:

  • 第一个参数是一个数组,数组中存放普通的字符串,例如 str 中的 “Welcome to ”、“course.”。
  • 在第一个参数之后的参数,都是每一个占位符的解释值,例如 str 中的 ${name}。

定义 tag 标签函数的格式:

function tag(literals, value1) {
  // 返回一个字符串
}

当字符串中的占位符比较多时,也可以使用不定参数的形式来定义后面的参数。

function tag(literals, ...values) {}

例子:

function tag(literals, ...values) {
  console.log(literals);
  console.log(values);
  let result = ""; // result 变量用来存放重组后的数组
  // 根据 values 的数量来确定遍历的次数
  for (let i = 0; i < values.length; i++) {
    result += literals[i];
    result += values[i];
    console.log(literals[i]);
    console.log(values[i]);
    console.log(result);
  }

  // 合并最后一个 literals
  result += literals[literals.length - 1];
  return result;
}
let name = `JavaScript`;
let str = tag`Welcome to ${name} course.`;
console.log(str);

在上面代码中定义了一个名为 tag 的标签,在其内部的执行逻辑如下:

  • 首先定义了一个名为 result 的空字符串用来存储最终输出字符串的结果。
  • 接下来执行了一个 for 循环,遍历了 values 的长度。
  • 取出 literals 的首个元素,再取出 values 中的首个元素,然后交替继续取出每一个元素,直到字符串拼接完成。

标签模板实际应用在两个方面:

  • 过滤 HTML 字符串,防止用户输入恶意内容。
  • 多语言的转换。

2、字符串的新增方法

判断指定字符串是否存在

在 ES5 中,我们要判断某个字符串是否包含指定字符串时,可以用 indexOf() 方法来判断,该方法可以返回指定字符串在某个字符串中首次出现的位置。
在 ES6 中,为我们新增了三种方法来判断字符串是否包含在其中。

  • includes():判断是否包含指定字符串,如果包含返回 true,反之 false。
  • startsWith():判断当前字符串是否以指定的子字符串开头,如果是则返回 true,反之 false。
  • endsWith():判断当前字符串是否以指定的子字符串结尾,如果是则返回 true,反之 false。

例子:

let str = "LanQiao Courses";
console.log("str 字符串中是否存在 Java:" + str.includes("Java"));
console.log("str 字符串的首部是否存在字符 Lan:" + str.startsWith("Lan"));
console.log("str 字符串的尾部是否存在字符 Course:" + str.endsWith("Course"));

在这里插入图片描述
注意:传入的字符串需要注意大小写,大小写不同也会造成匹配失败的情况。

重复字符串

repeat(n) 方法用于返回一个重复 n 次原字符串的新字符串,其参数 n 为整数,如果设置 n 为小数,会自动转换为整数。
例子:

let str = "HELLO";
console.log(str.repeat(4));
替换字符串

在 ES5 中有一个 replace() 方法可以替换指定字符串,不过它只能替换匹配到的第一个字符串,如果想匹配整个字符串中所有的指定字符串是很麻烦的。
在 ES6 中,为我们提供了 replaceAll() 方法来解决这个问题,它可以用来替换所有匹配的字符串。
其语法格式为:

string.replaceAll("待替换的字符", "替换后的新字符");

三、数组的扩展

1、创建数组的方法

Array.of()
let arr = new Array(5);
console.log("数组长度:" + arr.length);
console.log("arr[0]:" + arr[0]);
console.log("arr[4]:" + arr[4]);

从控制台显示可以看到打印出数组的长度为 5,Array() 中的 5 没有作为数组的元素被输出,而是被当作了数组的长度。

let arr = new Array(3, 4);
console.log("数组长度:" + arr.length);
console.log("arr[0]:" + arr[0]);
console.log("arr[1]:" + arr[1]);

参数 3 和 4 被看成了数组中的数据。在 Array() 中多写几个元素,可以看到它们都会被当成数组的元素。

let arr = new Array(4, "7", "8");
console.log("数组长度:" + arr.length);
console.log("arr[0]:" + arr[0]);
console.log("arr[1]:" + arr[1]);
console.log("arr[2]:" + arr[2]);

从控制台显示可以看到整数和字符类型均被当作了数组的元素。用上面这样的方式传入值是存在一定风险的。Array.of() 为我们解决了这个问题。
Array.of() 的语法格式如下:

Array.of(element 0, element 1, ..., element N)

返回具有 N 个元素的数组。

let arr = Array.of(7);
console.log("数组长度:" + arr.length);
console.log("arr[0]:" + arr[0]);

从控制台显示可以看出,当我们在 Array.of() 中只输入一个整数时,该参数不会被识别为数组的长度。
这样规则是统一的,写在 Array.of() 里面的内容都会被当作数组的元素。

Array.from()

在 ES6 之前,如果要把非数组类型的对象转换成一个数组,要用 [].slice.call() 把一个非数组类型变为数组类型。

let arrLike = {
  0: "🍎",
  1: "🍐",
  2: "🍊",
  3: "🍇",
  length: 4,
};
var arr = [].slice.call(arrLike);
console.log("arr:" + arr);

在 ES6 中为我们提供了 Array.from() 代替了这种旧办法。
Array.from() 方法可以将以下两类对象转为数组。

  • 类似数组的对象(array-like-object)。
  • 可遍历的对象(iterable-object)。

其基本使用格式为:

Array.from(待转换的对象);
let arrLike = {
  0: "🍎",
  1: "🍐",
  2: "🍊",
  3: "🍇",
  length: 4,
};
var arr = Array.from(arrLike);
console.log("arr:" + arr);

注意:Array.from() 方法是基于原来的对象创建的一个新数组。

2、数组实例的方法

find() 方法

find() 方法是用于从数组中寻找一个符合指定条件的值,该方法返回的是第一个符合条件的元素,如果没找到,则返回 undefined.
其语法格式为:

array.find(callback(value, index, arr), thisValue);

参数说明如下:

  • callback 是数组中每个元素执行的回调函数。
  • value 是当前元素的值,它是一个必须参数。
  • index 是数组元素的下标,它是一个可选参数。
  • arr 是被 find() 方法操作的数组,它是一个可选参数。
  • thisValue 是执行回调时用作 this 的对象,它是一个可选参数。
let arr = [1, 3, 4, 5];
let result = arr.find(function (value) {
  return value > 2;
});
console.log(result);

在控制台会打印出第一个大于 2 的数组元素。

let arr = [1, 3, 4, 5];
arr.find(function (value, index, arr) {
  console.log(value > 2);
  console.log(index);
  console.log(arr);
});

在代码中,我们返回了每次遍历判断条件的结果、当前元素的下标值、原数组。
在这里插入图片描述

findIndex() 方法

findIndex() 方法返回数组中第一个符合指定条件的元素的索引下标值,如果整个数组没有符合条件的元素,则返回 -1。
其语法格式为:

array.findIndex(callback(value, index, arr), thisArg);

参数说明如下:

  • callback 是数组中每个元素都会执行的回调函数。
  • value 是当前元素的值,它是一个必须参数。
  • index 是数组元素的下标,它是一个必须参数。
  • arr 是被 findIndex() 方法操作的数组,它是一个必须参数。
  • thisArg 是执行回调时用作 this 的对象,它是一个可选参数。

注意:执行回调函数时,会自动传入 value、index、arr 这三个参数。

let arr = ["小猫", "小狗", "兔子"];
let result = arr.findIndex(function (value, index, arr) {
  return value == "兔子";
});
console.log(result);

find() 方法和 findIndex() 方法,其实用法很像,最大区别在于两个方面:

  • 执行回调函数后的返回值不同,find() 方法返回的是元素本身,而 findIndex() 方法返回的是元素的下标。
  • 匹配失败的返回值不同,find() 方法返回的是 undefined,而 findIndex() 方法返回的是 -1。
fill() 方法

fill() 方法是用指定的值来填充原始数组的元素。
其使用格式为:

array.fill(value, start, end);

其参数说明如下:

  • value 是用来填充数组的值,它是一个必须参数。
  • start 是被填充数组的索引起始值,它是一个可选参数。
  • end 是被填充数组的索引结束值,它是一个可选参数。

注意:如果不指定 start 和 end 参数,该方法会默认填充整个数组的值。

let arr = ["🐱", "🐶", "🐰"];
let result = arr.fill("🐷");
console.log(result);
let arr = ["🐱", "🐶", "🐰", "🐍", "🐦", "🐟"];
let result = arr.fill("🐷", 2, 5);
console.log(result);
entries()、keys()、values()

entries()、keys()、values() 是 ES6 中三种数组的遍历方法,三个方法返回的都是 Array Iterator 对象。但三者之间不是完全相同。
entries() 方法以键/值对的形式返回数组的 [index,value],也就是索引和值。其语法格式为:

array.entries();

我们要输出 Array Iterator 对象里的值,可以用扩展运算符(…)来展开。否则看到结果只输出了 Array Iterator{},并没有以键值对的形式输出值。

let arr = ["🐱", "🐶", "🐰", "🐍", "🐦", "🐟"];
let result = arr.entries();
console.log(...result);

keys() 方法只返回数组元素的键值也就是元素对应的索引,不会返回其值。
其语法格式为:

array.keys();
let arr = ["🐱", "🐶", "🐰", "🐍", "🐦", "🐟"];
let result = arr.keys();
console.log(result);
console.log(...result);

values() 方法返回的是每个键对应的值。
其语法格式为:

array.values();
let arr = ["🐱", "🐶", "🐰", "🐍", "🐦", "🐟"];
let result = arr.values();
console.log(result);
console.log(...result);

在这里插入图片描述

3、for…of 循环

在使用 for 语句的时候,有一些局限性,如:

  • 必须要设置一个计数器,比如上面代码中的 i。
  • 必须有个退出循环的条件,如上面代码那样使用 length 属性获取数组的长度,当计数器大于等于数组长度时退出。

for…of 摆脱了计数器、退出条件等烦恼,它是通过迭代对象的值来循环的。它能迭代的数据结构很多,数组、字符串、列表等。
for…of 的语法格式如下所示:

for (variable of iterable) {
}

参数说明如下:

  • variable:是存放当前迭代对象值的变量,该变量能用 const、let、var 关键字来声明。
  • iterable:是可迭代对象。
const arr = ["小红", "小蓝", "小绿"];
for (let name of arr) {
    document.write("欢迎" + name + "来到河南农业科技大学!" + "<br/>");
}

4、扩展运算符

扩展运算符(…)是 ES6 的新语法,它可以将可迭代对象的参数在语法层面上进行展开。
其语法格式为:

// 在数组中的使用
let VariableName = [...value];

例子:

let animals = ["兔子🐰", "猫咪🐱"];
let zoo = [...animals, "老虎🐯", "乌龟🐢", "鱼🐟"];
console.log(zoo);
let animals = ["老虎🐯", "乌龟🐢", "鱼🐟"];
let newAnimals = [...animals];
console.log(newAnimals);

从上面的例子我们可以这么理解:使用扩展运算符可以起到将数组展开的作用。
在 ES2018 版本前的它有一个缺点就是只能用在数组和参数上。于是在 ES2018 中又将扩展运算符引入了对象。
在对象上,我们主要有以下三种操作:

  • 可以使用扩展运算符将一个对象的全部属性插入到另一个对象中,来创建一个新的对象
  • 可以使用扩展运算符给对象添加属性
  • 可以使用扩展运算符合并两个新对象
let student = { name: "小白", age: 17, email: "1234@qq.com" };
let NewObj = { ...student };
console.log(NewObj);
let student = { name: "小白", age: 17, email: "1234@qq.com" };
let NewObj = { ...student, id: 7 };
console.log(NewObj);
let studentName = { name: "小白" };
let studentAge = { age: 17 };
let NewObj = { ...studentName, ...studentAge };
console.log(NewObj);

四、函数的扩展

1、默认参数

回忆一下,ES5 中默认参数的使用。

function func(words, name) {
  name = name || "🍎";
  console.log(words, name);
}
func("大家好!我是");
func("大家好!我是", "🍐");
func("大家好!我是", "");

在上面代码中:

  • func 函数一共有两个形式参数,在 func(‘大家好!我是’) 我们只指定了第一个形式参数 words 的值,所以 name 使用的是默认值「🍎」。
  • 在 func(‘大家好!我是’,‘🍐’) 中我们指定了两个形式参数的值,所以 name 没有使用默认值。
  • 在 func(‘大家好!我是’,‘’) 中我们指定第二个形式参数为空字符串,被认为是没有赋值,所以也使用的是默认值。
在函数中直接设置默认值

在 ES6 中我们可以直接在函数的形参里设置默认值。

function func(words, name = "🍎") {
  console.log(words, name);
}
func("请给我一个");
func("请给我一个", "🍐");
func("请给我一个", "");

在上面代码中:

  • func(‘请给我一个’) 只传入了第一个参数,第二个参数没传入任何值,故第二个参数使用了默认值🍎。
  • func(‘请给我一个’,‘🍐’) 传入了二个参数,所以第二个参数没有使用默认值。
  • func(‘请给我一个’,‘’) 第二个参数,虽然传入的是空字符串,空字符串也算是一个参数值,故同样不会使用默认值。

参数变量是默认声明的,我们不能用 let 或者 const 再次声明。

function func(words, name = "🍎") {
  let words = "我需要一个";
  console.log(words, name);
}
func("请给我一个", "🍐");

此时控制台报错。
注意参数默认值的位置
设置默认值的参数,一般放在其他参数的后面,也就是尾部。

使用函数作为默认值

我们还可以使用自定义的函数作为形式参数的默认值。

function parameter() {
  return "🖤";
}
function func(words, name = parameter()) {
  console.log(words, name);
}
func("请给我一颗小");
func("请给我一颗小", "💗");
解构参数

解构可以用在函数参数传递的过程中。

function func(name, value, mount, { a, b, c, d = "苹果" }) {
  console.log(`${name}${value}元钱买了${mount}${d}`);
  console.log(`${name}${value}元钱买了${mount}${c}`);
}
func("小蓝", 5, 3, {
  a: "西瓜",
  b: "菠萝",
  c: "桃子",
});

在上面代码中:

  • func 函数包含 4 个参数,其中第 4 个参数是解构参数,解构参数里面包含 4 个参数变量 a、b、c、d。
  • 使用 func(‘小蓝’,5,3,{a:‘西瓜’,b:‘菠萝’,c:‘桃子’}) 调用该函数,其中传入 name 参数的值为“小蓝”,value 参数的值为 5,mount 参数的值为 3;解构参数只传入三个值,a 的值为“西瓜”,b 的值为“菠萝”,c 的值为“桃子”,d 使用的是默认值。

从上面代码中我们可以看出,解构参数是以键值对的形式传入的,其参数名与键值名保持一致,即可传入成功。

2、rest 参数

rest 参数又称为剩余参数,用于获取函数的多余参数。rest 参数搭配的变量是一个数组,该变量将多余的参数放入数组中。
rest 参数和扩展运算符在写法上一样,都是三点(…),但是两者的使用上是截然不同的。
扩展运算符就像是 rest 参数的逆运算,主要用于以下几个方面:

  • 改变函数的调用。
  • 数组构造。
  • 数组解构。

rest 参数语法格式为:

// 剩余参数必须是函数的最后一个参数
myfunction(parameters, ...rest);
function func(a, b, ...rest) {
  console.log(rest);
}
func(1, 2, 3, 4, 5, 6, 7, 8, 10);

在上面代码中,我们给 func 函数传了 10 个参数,形式参数 a 和 b 各取一个值,多余的 8 个参数都由 rest 参数收了。
在这里插入图片描述

function func(a,...rest,b){
    console.log(a);
    console.log(rest);
    console.log(b);
}
func(1,2,3,4,5,6,7,8,10);

因为 rest 参数只能作为函数的最后一个参数,若把它放在中间,在控制台则可以看到报错。

3、箭头函数

箭头函数,顾名思义,就是用箭头(=>)来表示函数。箭头函数和普通函数都是用来定义函数的,但两者在语法构成上非常不同。
箭头函数的基本用法:

(param1,param2,...,paramN) => {expression}

箭头前面 () 里放着函数的参数,箭头后面 {} 里放着函数内部执行的表达式。
普通函数:

let sum = function (a, b) {
  return a + b;
};
console.log(sum(1, 2));

箭头函数:

let sum = (a, b) => a + b;
console.log(sum(1, 2));

箭头函数除了代码简洁以外,它还解决了匿名函数的 this 指向问题。

4、this 的指向

this 是指向调用包含自身函数对应的对象。
假如,有一个函数能够实现在指定时间周期内一直计数。用 ES5 的语法实现如下所示:

function Number() {
  var that = this;
  that.num = 1;
  setInterval(function count() {
    // count 函数指向的是 that 变量
    that.num++;
  }, 1000);
}

在上面例子中,定义了一个名 Number() 的函数,该函数里包含一个 num 参数。在 Number() 函数里有一个 count() 的函数,在其内部让 num 自增。count 函数是作为 setInterval() 函数的参数而存在的,setInterval() 函数的作用是在某个周期内自动调用函数。
可以看到上面的两个函数中,每个函数会定义自己的 this,this 只存在于你所使用的作用域范围内。
而在箭头函数中的 this 对象,就是定义该函数所在的作用域所指向的对象,而不是使用所在作用域指向的对象。

function Number() {
  this.num = 1;
  setInterval(() => {
    // this 正确地引用了 Number 对象
    this.num++;
  }, 1000);
}

箭头函数与普通函数的区别:

  • 箭头函数的 this 指向是其上下文的 this,没有方法可以改变其指向。
  • 普通函数的 this 指向调用它的那个对象。
不带参数的箭头函数
let dogName = () => "😊";
console.log(dogName());

ES5 语法格式的代码:

let dogName = function () {
  return "😊";
};
console.log(dogName());
带默认参数的箭头函数

箭头函数与普通函数一样也是可以直接给参数设置默认值的。

let func = (x, y = "🌈") => {
  return `${x}${y}`;
};
console.log(func("请给我一道"));
console.log(func("请给我一朵", "🌺"));

在上面代码中,定义的 func 函数里有两个形参,第二个形式参数里设置默认值为彩虹,函数内部使用模板字面量的方式返回了两个形式参数的值。
1. 当箭头函数的参数为单个的时候,可以省略包裹参数的小括号。

let func = a => {
  return a;
}
console.log(func('嗨!我是单参数'));

2. 当 return 后面为单语句时,可以省略 return 和 {} 花括号。

let func = (a) => a;
console.log(func("可以省略我哦~"));

3. 如果箭头函数直接返回一个对象,需要用小括号包裹,否则就会报错。

let student = () => ({ name: "小蓝" });
console.log(student());
带 rest 参数的箭头函数
let func = (a, b, ...rest) => {
  console.log(a);
  console.log(b);
  console.log(rest);
};
func(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);

在上面代码中,我们给 func 函数传入了 10 个参数,其中,1 赋给形式参数 a,2 赋给形式参数 b,剩余的参数都由 rest 参数收下了。

五、类的扩展

1、类的声明

类的介绍

在 ES6 之前其实是不存在类的,因为 JavaScript 并不是一门基于类的语言,它使用函数来创建对象,并通过原型将它们关联在一起。
先来看个 ES5 语法的近类结构,首先创建一个构造函数,然后定义另一个方法并赋值给构造函数的原型。

function MyClass(num) {
  this.num = num;
  this.enginesActive = false;
}
MyClass.prototype.startEngines = function () {
  console.log("starting ...");
  this.enginesActive = true;
};
const myclass = new MyClass(1);
myclass.startEngines();
  1. MyClass 是一个构造函数,用来创建 MyClass 类型的对象。
  2. 使用 this 声明并初始化 MyClass 的属性 num 和 enginesActive。
  3. 并在原型中存储了一个可供所有实例调用的方法 startEngines。
  4. 然后使用 “new+构造函数”的方式创建实例对象 myclass,并将 myclass 的 num 属性初始化为 1。
  5. 最后调用执行实例对象 myclass 的 startEngines 方法。

在 ES6 中,提供了 class 关键字来创建类。类是用来构建对象的蓝图。蓝图就是一个物体的结构,可以在这个结构的基础上定义不同的属性,比如一件相同款式的衣服,有不同的颜色,衣服相当于蓝图,颜色相当于属性。
在这里插入图片描述

class Clothes {
  // constructor 是构造函数
  constructor(color, size) {
    this.size = size;
    this.color = color;
  }
}
const xiaoLan = new Clothes("黑色", "L");
const xiaoBai = new Clothes("蓝色", "M");

在上面代码中 constructor 是类的构造函数,它是定义类的默认方法,当你使用 new 来创建对象实例化时,会自动调用该方法。如果没有在类中添加 constructor,也会默认有一个 constructor 方法,所以它在类中是必须有的。

class MyClass {
  // constructor 方法是类的默认方法
  constructor(num) {
    this.num = num;
    this.enginesActive = false;
  }
  // 相当于 MyClass.prototype.startEngines
  startEngines() {
    console.log("staring...");
    this.enginesActive = true;
  }
}
const myclass = new MyClass(1);
myclass.startEngines();

在上面代码中,MyClass 的类声明其实与之前构造函数的声明过程是相似的,只是在使用 class 关键字进行类声明中通过特殊的 constructor 方法名来定义了构造函数,且由这种类声明的简洁语法来定义方法。
所以,ES6 的 class 关键字只是一个语法糖,其底层还是函数和原型,只是给它们披上了一件衣服而已。

类表达式

类和函数都有两种存在形式:

  • 声明形式(例如:function、class 关键字声明)。
  • 表达式形式(例如:const A = class{})。

ES5 语法:

function DogType(name) {
  this.name = name;
}
DogType.prototype.sayName = function () {
  console.log("大家好!我是一只小" + this.name + "。");
};
let dog = new DogType("柯基");
dog.sayName();
console.log(dog instanceof DogType);
console.log(dog instanceof Object);

(1)匿名类表达式
ES6语法:

// ES6 语法
let DogType = class {
  constructor(name) {
    this.name = name;
  }
  sayName() {
    console.log(`大家好!我是一只小${this.name}`);
  }
};
let dog = new DogType("柯基");
dog.sayName();
console.log(dog instanceof DogType);
console.log(dog instanceof Object);

在上面代码中 constructor(name){} 等价于 DogType 的构造函数,sayName(){} 等价于 DogType.prototype.sayName = function(){}。
(2)命名类表达式
在 let DogType = class 我们定义的是一个匿名类表达式,和函数一样,我们也可以给类表达式命名。

let DogName = class MyClass {
  constructor(name) {
    this.name = name;
  }
  sayName() {
    console.log(this.name);
  }
};
console.log(typeof DogName);
console.log(typeof MyClass);

在上面代码中,类表达式被命名为 MyClass,该标识符只存在于类定义中,而在类外部是不存在 MyClass 的,所以当我们在类外部查看其类型时,会输出 undefined。
举上面的例子有两个目的,一是告诉我们可以在类表达式中命名类名;二是类名不能在类外使用。

2、类的继承

类的继承介绍

在 ES6之前,通常是使用构造函数 + 原型对象的方式去模拟实现继承的功能。

function Animal(name, age, speed) {
  this.name = name;
  this.age = age;
  this.speed = speed;
}
Animal.prototype.run = function () {
  console.log(`${this.age}岁的${this.name}酷跑了 ${this.speed} 公里。`);
};
Animal.prototype.stop = function () {
  console.log(`${this.name}停止了奔跑。`);
};
let dog = new Animal("闷墩儿", "一", 5);
dog.run();
dog.stop();

在 ES6 中为我们提供了 extends 关键字来实现继承。
其使用格式如下:

class child_class_name extends parent_class_name {}

上面代码的意思是 child_class_name 类继承了 parent_class_name 类。

class Animal {
  constructor(name, age, speed) {
    this.name = name;
    this.age = age;
    this.speed = speed;
  }
  run() {
    console.log(`${this.age}岁的${this.name}酷跑了 ${this.speed} 公里。`);
  }
  stop() {
    console.log(`${this.name}停止了奔跑。`);
  }
}
class Dog extends Animal {} // Dog 类继承了 Animal 类
// 实例化 Dog 类
let dog = new Dog("闷墩儿", "一", 5);
// 调用 Animal 类中的 run() 方法和 stop() 方法
dog.run();
dog.stop();
  • 使用 class Dog extends Animal {} 定义了一个 Dog 类继承了 Animal 类的属性和方法。
  • 使用 let dog = new Dog(‘闷墩儿’,‘一’,5) 实例化 Dog 对象。
  • 使用 Dog 类的实例对象 dog 去调用 run 和 stop 方法,程序会先在 Dog 类的原型 Dog.prototype 中查找两个方法是否存在,找不到的情况下会去其父类原型 Animal.prototype 中查找,以此类推,层层向上最终找到并调用执行。

在这里插入图片描述
extends 后可以接表达式
extends 关键字的后面,不仅仅可以跟类,它还可以接一个表达式。
例如:定一个生成父类的函数。

function func(message) {
  return class {
    say() {
      console.log(message);
    }
  };
}
class Person extends func("欢迎来到河南农业科技大学!") {}
person = new Person();
person.say();
  • 先创建一个返回匿名类的函数 func,该匿名类中包含一个方法 say()。
  • 然后声明 Person 类继承了 func 函数的返回值(也就是包含方法 say() 的匿名类)。
  • 在上一步继承前,先执行 func 函数,并向其传入参数“欢迎来到河南农业科技大学!”。
  • 接着 new 了一个 Person 类的实例对象 person。
  • 然后 person 调用 say() 方法,最终在控制台输出了 “欢迎来到河南农业科技大学!”。
认识 super

ES6 为我们提供了超级函数 super 我们的继承变得完整且具备可扩展性。
它的使用格式有两种:

  • 使用 super.method(…) 来调用父方法。
  • 使用 super(…) 调用父构造函数。
class Dog extends Animal {
  run() {
    super.run();
    console.log(`${this.name}开始奔跑了。`);
  }
}

重写构造函数
在上面的例子中,我们并没有在 Dog 类中创建 constructor。
前面说过,类中不写 constructor 的情况下,在被实例化时会自动生成一个 constructor,如下所示:

class Dog extends Animal {
  constructor(...args) {
    super(...args);
  }
}

可以看到自动生成的 constructor 中只有一个 super(…args);,执行该 super 函数可以继承并初始化父类 Animal 中构造函数里的属性。

在 JavaScript 中,继承类的构造函数和其他函数有区别。继承类的构造函数有一个特殊的内部属性 [[ConstructorKind]]:“derived”。通过该属性会影响 new 的执行:

  • 当一个普通(即没有父类的类)的构造函数运行时,它会创建一个空对象作为 this,然后继续运行。
  • 但是当子类的构造函数运行时,与上面说的不同,它将调用父构造函数来完成这项工作。

所以,继承类的构造函数必须调用 super() 才能执行其父类的构造函数,否则 this 不会创建对象。

class Dog extends Animal {
  constructor(name, age, speed, species) {
    super(name);
    this.species = species;
  }
  run() {
    console.log(`${this.name}是一只奔跑的${this.species}`);
  }
}
let dog = new Dog("闷墩儿", "一", 5, "狗");
dog.run();

使用 super() 的注意事项:

  • 只能在继承类中使用 super()。
  • 构造函数中,一定要先调用 super() 再访问 this。

3、类的属性和方法

静态方法

在 JavaScript 中,静态方法是给面向对象编程提供的类方法,这种类方法有点类似于 Ruby 语言。
静态方法的好处是不需要实例化类,就可以直接通过类名去访问,这样不需要消耗资源反复创建对象。
在 ES6 之前,要模仿静态方法的常规操作是将方法直接添加到构造函数中。
ES5语法:

// ES5 语法
function DogType(name) {
  this.name = name;
}
// 静态方法
DogType.create = function (name) {
  return new DogType(name);
};
// 实例方法
DogType.prototype.sayName = function () {
  console.log(`大家好!我是一只小${this.name}`);
};
let dog = DogType.create("柯基");
dog.sayName();
  • 创建一个构造函数 DogType。
  • 以“类名.方法名”的方式为其定义一个静态方法 create,该方法最终创建一个 DogType 实例。
  • 在其原型上添加 sayName 方法。
  • 使用“类名.方法名”的方式调用其静态方法 create 创建一个实例对象 dog。
  • 使用 dog 调用 sayName 方法输出自我介绍的信息。

由于 create 方法可以直接通过类名去访问,不需在被实例化时创建,因而可以被认为是 DogType 类的一个静态方法。
ES6 为我们提供了 static 关键字来定义静态方法。
其使用格式为:

static methodName(){
}

ES6语法:

// ES6 语法
class DogType {
  constructor(name) {
    this.name = name;
  }
  // 对应 DogType.prototype.sayName
  sayName() {
    console.log(`大家好!我是一只小${this.name}`);
  }
  // 对应 DogType.create
  static create(name) {
    return new DogType(name);
  }
}
let dog = DogType.create("柯基");
dog.sayName();

在上面代码中,使用 static create 创建了静态方法 create,并返回实例化的 DogType,它相当于 ES5 代码中的 DogType.create = function(name){},两者实现的功能相同,区别在于 ES6 使用了 static 关键字来标识这是个静态方法。
如果静态方法中包含 this 关键字,这个 this 关键字指的是类,而不是实例。

class MyClass {
  static method1() {
    this.method2();
  }
  static method2() {
    console.log(this);
  }
}
MyClass.method1();

在上面代码中,MyClass 类中定义了两个静态方法 method1 和 method2,静态方法 method1 调用了 this.method2,其中 this 指向的是 MyClass 类而不是 MyClass 的实例,这相当于 MyClass.method2。
观察上面的代码可以发现,我们没有创建实例化对象,直接用「类名.方法名」就可以访问该方法,这就是静态方法的特点了。除了这个特点外,静态方法还不能被其实例调用。

class MyClass {
  static method1() {
    this.method2();
  }
  static method2() {
    console.log("hello,welcome to there!");
  }
}
let myclass = new MyClass();
myclass.method2();

可以看到使用 myclass 实例对象调用 method2 静态方法会报错,则说明静态方法只能通过“类名.方法名”调用。

静态属性

static 关键字除了可以用来定义静态方法外,还可以用于定义静态属性。
静态属性是指类本身的属性(Class.propName),不是定义在实例对象上的属性。
静态属性具有全局唯一性,静态属性只有一个值,任何一次修改都是全局性的影响。
当我们类中需要这么一个具有全局性的属性时,我们可以使用静态属性。
最初定义静态属性写法如下:

class Dog {}
Dog.dogName = "二狗";
console.log(Dog.dogName); // 二狗

在上面代码中,我们为 Dog 类定义了一个静态属性 dogName。
使用 static 关键字 来定义静态属性。
静态属性的使用格式为:

static propName = propVaule;
class Dog {
  static dogName = "二狗;
}
console.log(Dog.dogName); // 二狗
静态属性和方法的继承

另外静态方法和静态属性是可以被继承的。

class Animal {
  static place = "游乐园";
  constructor(name, speed) {
    this.name = name;
    this.speed = speed;
  }
  // place 静态属性是类本身的属性,不是实例对象,所以这里不能用 this.place
  run() {
    console.log(
      `名为${this.name}的狗狗在${Animal.place}里酷跑了 ${this.speed} 公里。`
    );
  }
  static compare(animal1, animal2) {
    return animal1.speed - animal2.speed;
  }
}
class Dog extends Animal {}
// 实例化
let dogs = [new Dog("二狗", 7), new Dog("乐乐", 4)];
dogs[0].run();
dogs[1].run();
// 继承静态方法
console.log(`二狗比乐乐多跑了 ${Dog.compare(dogs[0], dogs[1])} 公里。`);
// 继承静态属性
console.log(`奔跑地点:${Dog.place}`);
  • Animal 类中声明了静态属性 place 和静态方法 compare。并在 run 方法中使用 Animal.place
    调用了其静态属性。
  • Dog 继承 Animal 类,但没有定义任何自己的属性和方法。
  • 实例化两个 Dog 类对象“乐乐”和“二狗”,并分别调用其 run 方法。
  • 分别使用 Dog.compare 和 Dog.place 调用了其父类的静态方法和属性。
私有方法和私有属性

在某些情况下,如果外部程序可以随意修改类中的属性或调用其方法,将会导致严重的错误。基于这个问题,我们就在类中引入了私有化的概念,私有属性和方法能够降低它们与外界的耦合度,避免很多问题。
在面向对象编程中,关于属性和方法的访问有以下两种情况:

  • 类的属性和方法,在其内部和外部均可进行访问,也称为公共属性和方法
  • 类的属性和方法,只能在类中访问,不能在类之外的其他地方访问,也称为私有属性和方法

在 ES6 的类中使用 # 可以设置私有方法和私有属性。
其语法格式为:

// 私有属性
#propertiesName;
// 私有方法
#methodName;

因为 JavaScript 是一门动态语言,没有类型声明,使用独立的符号方便可靠,更容易区分。

class Galaxy {
  #address = `X 星系`;
  constructor(name) {
    this.name = name;
  }
}
let alien = new Galaxy();
console.log(alien.#address);

控制台报错,私有属性不能在类的外部被访问。

class Galaxy {
  #address = `X 星系`;
  constructor(name) {
    this.name = name;
  }
  message() {
    console.log(`${this.name}住在${this.#address}`);
  }
}
let alien = new Galaxy("小7");
alien.message();

可以看到,在 Galaxy 类中可以访问 #address 私有属性。

class Galaxy {
  #address = `X 星系`; // 私有属性 address
  constructor(name) {
    this.name = name;
  }
  #message() {
    // 私有方法 message
    return `${this.name}住在${this.#address}`;
  }
  say() {
    // 访问私有方法
    console.log(this.#message());
  }
}
let alien = new Galaxy("小7");
alien.say();

在上面代码中,Galaxy 类里定义了私有方法 message(),在方法里返回了一句话(其中包含公有属性 name 和私有属性 address);在公有方法 say() 中我们访问了私有方法 message()。

4、new.target 属性

介绍

ES6 为我们提供了 new.target 属性去检查函数或者类的构造函数中是否使用 new 命令。
在构造函数中,若一个构造函数不是使用 new 来调用的,new.target 会返回 undefined。

class Person {
  constructor(name) {
    this.name = name;
    console.log(new.target.name);
  }
}
class Occupation extends Person {
  constructor(name, occupation) {
    super(name);
    this.occupation = occupation;
  }
}
let person = new Person("小白");
let occupation = new Occupation("小蓝", "前端工程师");

在上面代码中,使用 new.target.name 用来输出对应实例对象的类名。

class Person {
  constructor(name) {
    this.name = name;
  }
  static say() {
    console.log(new.target);
  }
}
class Occupation extends Person {
  constructor(name, occupation) {
    super(name);
    this.occupation = occupation;
  }
}
Person.say();
Occupation.say();

不用 new 去实例化 Person 和 Occupation ,就会输出 undefined。

应用场景

当我们想写不能被实例化,必须在继承后才能使用的类时,我们可以用 new.target 属性,做为限制其不能被实例化(new)的条件。

class Person {
  constructor() {
    // 如果实例化对象使用的是 Person 类,则抛出错误
    if (new.target === Person) {
      throw new Error("Person 类不能被实例化。");
    }
  }
}
class Occupation extends Person {
  constructor(name, occupation) {
    super();
  }
}
let person1 = new Person();

运行后可以看到控制台报错,提示不能去实例化 Person。

class Person {
  constructor() {
    // 如果实例化对象使用的是 Person 类,则抛出错误
    if (new.target === Person) {
      throw new Error("Person 类不能被实例化。");
    }
  }
}
class Occupation extends Person {
  constructor(name, occupation) {
    super();
    this.name = name;
    this.occupation = occupation;
    console.log(`${name}${occupation}`);
  }
}
let occupation = new Occupation("小蓝", "前端工程师");

实例化 Occupation 类,从控制台的输出结果可以看到,能成功访问 Occupation。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值