在当今的前端开发领域,ES6(ECMAScript 2015)已成为 JavaScript 编程的核心标准,其带来的众多新特性极大地改变了我们编写 JavaScript 代码的方式。对于求职者而言,深入理解 ES6 新特性在面试中至关重要,下面将为大家详细梳理这些关键知识点。
一、变量声明:let 与 const
在 ES6 之前,var 是声明变量的主要方式,但它存在一些问题,比如函数作用域和变量提升现象。
函数作用域问题
var声明的变量具有函数作用域,而非块级作用域。这意味着变量在整个函数内部都是可见的,即使在条件语句块或循环语句块中声明,其作用域也不会局限于该块。例如:
function test() {
if (true) {
var localVar = 'I am in if block';
}
console.log(localVar); // 输出: I am in if block
}
test();
- 上述代码中,
localVar在if块内声明,按照常理,它的作用域应局限于if块。 - 但由于
var的函数作用域特性,在函数test的任何位置都能访问到localVar。 - 这可能导致变量命名冲突,比如在函数内多个不同逻辑块中使用相同的变量名,后声明的变量可能覆盖先声明的变量,影响程序逻辑。
变量提升现象
- 变量提升指的是,使用
var声明的变量,无论在函数中的何处声明,都会被提升到函数的顶部,就好像它们在函数开头已经声明好了。 - 但只有声明会被提升,初始化(赋值操作)不会被提升。
function example() {
console.log(x); // 输出: undefined
var x = 10;
console.log(x); // 输出: 10
}
example();
- 例子中,
var x的声明被提升到了函数example的顶部,所以在console.log(x)执行时,变量x已经被声明了,但还没有被赋值,因此输出undefined。 - 在代码执行到
x = 10时,才对变量x进行赋值,第二个console.log(x)输出10。 - 变量提升容易让代码阅读者产生误解,增加代码理解和调试的难度。
let 和 const 关键字
ES6 引入了 let 和 const 关键字,它们具有块级作用域。例如,在一个 if 语句块中使用 let 声明的变量,仅在该 if 块内有效,出了这个块就无法访问。
if (true) {
let localVar = 'I am local to this block';
console.log(localVar); // 输出: I am local to this block
}
console.log(localVar); // 报错: localVar is not defined
const 用于声明常量,一旦赋值便不可更改,这在定义一些不会变动的配置项等场景中很有用。
const PI = 3.1415926;
PI = 3.14; // 报错: Assignment to constant variable.
二、函数参数:默认值、剩余参数与扩展运算符
为参数赋予默认值
在 ES6 之前,当函数希望在没有接收到某个参数(即参数值为undefined)时使用一个默认值,就需要手动编写代码如if语句来检查参数是否为空,然后决定是使用传入的参数值还是使用默认值。
ES6 允许直接在函数的参数定义处指定参数的默认值。这极大地简化了参数判空逻辑。
function greet(name = 'Guest') {
console.log(`Hello, ${name}!`);
}
greet(); // 输出: Hello, Guest!
- 当调用函数时未传入该参数,或者传入的值为 undefined,函数便会使用默认值。
- 若传入的值为 null,函数并不会使用默认值。
- 函数参数是否使用默认值是基于参数是否为
undefined来判断的。 null是一个有效的值,所以函数会直接使用传入的 null。
- 函数参数是否使用默认值是基于参数是否为
- 默认值的设置是惰性求值的。
- 惰性求值:程序执行过程中,只有当需要使用某个表达式的值时,才对该表达式进行求值,而不是在表达式定义时就立即求值。
- 即默认值的计算不是在函数定义时进行,而是在函数调用且该参数未传入(或为
undefined)时才进行计算。
剩余参数的运用
在 ES6 之前,处理不定数量的参数往往依赖 arguments 对象,它是函数内部用于获取所有传入参数的类数组对象,而并非真正的数组,只能使用数组的length属性如 和通过索引查找元素的方法。
剩余参数(...)可将多个参数收集为数组,替代了传统的 arguments 对象。
function sum(...numbers) {
return numbers.reduce((acc, curr) => acc + curr, 0);
}
sum(1, 2, 3); // 输出: 6
剩余参数必须是函数参数列表中的最后一个参数,否则会引发语法错误。并且,函数的 length 属性不会包含剩余参数,仅统计正常参数的个数。
扩展运算符
扩展运算符同样使用三个点号(...),它能将数组或对象展开,在复制、合并等操作中发挥着重要作用。
展开数组
把数组里的元素展开成一个个独立的元素,这在合并数组、复制数组等场景中十分有用。对象操作同理。
// 合并数组
const arr1 = [1, 2, 3];
const arr2 = [4, 5, 6];
const combinedArr = [...arr1, ...arr2];
console.log(combinedArr); // 输出: [1, 2, 3, 4, 5, 6]
// 复制数组
const originalArr = [1, 2, 3];
const copiedArr = [...originalArr];
console.log(copiedArr); // 输出: [1, 2, 3]
在合并数组时,扩展运算符相较于传统的 concat 方法,代码更为简洁直观;在复制数组或对象时,虽然对于一维数组或对象的第一层是深拷贝,但对于多维数组或对象内部嵌套的对象,仍是浅拷贝。
传递数组元素给函数
可将数组元素当作独立参数传递给函数。
function sum(a, b, c) {
return a + b + c;
}
const numbers = [1, 2, 3];
const result = sum(...numbers);
console.log(result); // 输出: 6
函数参数解构
在函数参数中使用扩展运算符来解构对象。
function printInfo({ name, ...rest }) {
console.log(`Name: ${name}`);
console.log('Other info:', rest);
}
const person = { name: 'John', age: 30, city: 'New York' };
printInfo(person);
// 输出:
// Name: John
// Other info: { age: 30, city: 'New York' }
字符串操作
能把字符串拆分成字符数组。
const str = 'hello';
const charArray = [...str];
console.log(charArray); // 输出: ['h', 'e', 'l', 'l', 'o']
类数组对象转换为数组
将类数组对象(如 arguments 对象、NodeList 等)转换成真正的数组,这样就能使用数组的方法了。
function convertArguments() {
const args = [...arguments];
console.log(args.map(arg => arg * 2));
}
convertArguments(1, 2, 3); // 输出: [2, 4, 6]
三、Symbol:全新的原始数据类型
ES6 引入的 Symbol 类型是一种原始数据类型,其主要用途是创建唯一标识符。
每个 Symbol 标识符都是独一无二的
Symbol 的创建机制是每次调用 Symbol() 函数时,JavaScript 引擎会在内存中为新创建的 Symbol 值分配一个全新且唯一的标识符。
- 这里所说的 “标识符” 并不是我们常见的像变量名、属性名那样可以直接看到和操作的文本字符串,而是 JavaScript 引擎内部用于区分不同
Symbol值的一个标识。从开发者的角度它是隐藏的,无法直接获取和使用这个内部标识符的具体内容。
描述只是作为这个 Symbol 的一个可选的附加信息,用于开发者在调试代码时更好地识别该 Symbol,但并不会影响 Symbol 本身的唯一性。
const symbolA = Symbol('test');
const symbolB = Symbol('test');
console.log(symbolA === symbolB);
尽管 symbolA 和 symbolB 的描述都是 'test',但它们是不同的 Symbol 值,因此 symbolA === symbolB 的结果为 false。
避免对象属性名冲突
当使用字符串作为对象的属性名时,很容易出现属性名冲突的情况。
假设要开发一个简单的游戏角色系统,有多个不同的技能模块会为角色对象添加技能属性。以下分别展示使用字符串属性名导致冲突,以及使用 Symbol 避免冲突的情况。
反例:使用字符串属性名导致冲突
// 定义两个技能模块,都使用字符串 '技能' 作为属性名
function addAttackSkill(character) {
character['技能'] = '攻击技能';
return character;
}
function addDefenseSkill(character) {
character['技能'] = '防御技能';
return character;
}
// 创建一个角色对象
const character = {};
// 依次添加技能
const characterWithSkills = addDefenseSkill(addAttackSkill(character));
// 尝试访问技能属性
console.log('角色的技能:', characterWithSkills['技能']);
该例中,两个技能模块都使用了字符串 '技能' 作为属性名。
当我们先添加攻击技能,再添加防御技能时,后添加的防御技能属性覆盖了前面的攻击技能属性。最终输出的结果是 '防御技能',这表明前面的属性被意外覆盖,造成了数据丢失,这就是属性名冲突带来的问题。
正例:使用 Symbol 避免属性名冲突
// 定义两个 Symbol 类型的技能属性名
const attackSkillSymbol = Symbol('技能');
const defenseSkillSymbol = Symbol('技能');
// 定义两个技能模块,使用 Symbol 作为属性名
function addAttackSkillWithSymbol(character) {
character[attackSkillSymbol] = '攻击技能';
return character;
}
function addDefenseSkillWithSymbol(character) {
character[defenseSkillSymbol] = '防御技能';
return character;
}
// 创建一个角色对象
const anotherCharacter = {};
// 依次添加技能
const anotherCharacterWithSkills = addDefenseSkillWithSymbol(addAttackSkillWithSymbol(anotherCharacter));
// 尝试访问技能属性
console.log('角色的攻击技能:', anotherCharacterWithSkills[attackSkillSymbol]);
console.log('角色的防御技能:', anotherCharacterWithSkills[defenseSkillSymbol]);
该例中,我们使用 Symbol 来定义技能属性名。
- 虽然
attackSkillSymbol和defenseSkillSymbol的描述都是'技能',但它们是两个不同的Symbol值。 - 当我们依次添加攻击技能和防御技能时,这两个技能属性可以独立地存储在角色对象中,不会发生冲突。最终我们可以分别访问到角色的攻击技能和防御技能,这保证了数据的完整性。
四、数据结构:Set 与 Map
Set:存储唯一值的集合
Set 对象允许你存储任何类型的唯一值,无论是原始值还是对象引用。这意味着在 Set 中不会有重复的值,它会自动对存储的值进行去重处理。
创建方式
使用 new Set() 构造函数来创建一个 Set 对象,它可以接受一个可迭代对象(如数组、字符串等)作为初始值。
特性
- 唯一性:
Set最重要的特性就是它会自动去重,确保集合中的每个值都是唯一的。如上述使用数组[1, 2, 2, 3]初始化Set时,重复的2只会保留一个。 - 无序性:
Set中的元素没有特定的顺序,不能像数组那样通过索引来访问元素。
常用方法
-
add(value):向Set中添加一个新元素,如果该元素已经存在,则不会添加。返回Set对象本身,因此可以链式调用。 -
delete(value):从Set中删除指定元素,如果元素存在并成功删除,返回true;否则返回false。 -
has(value):检查Set中是否存在指定元素,存在返回true,否则返回false。 -
clear():移除Set中的所有元素。 -
size:获取Set中元素的数量,类似于数组的length属性。
const array = [1, 2, 2, 3, 3, 4];
const mixedSet = new Set(array);
// 添加原始值
mixedSet.add(5);
mixedSet.add('apple');
mixedSet.add(true);
// 创建对象并添加对象引用
const person1 = { name: 'Alice', age: 25 };
const person2 = { name: 'Bob', age: 30 };
mixedSet.add(person1);
mixedSet.add(person2);
// 尝试添加重复的原始值和对象引用
mixedSet.add(5);
mixedSet.add(person1);
// 检查元素是否存在
console.log('Set 中是否存在数字 5:', mixedSet.has(5));
console.log('Set 中是否存在 person2 对象:', mixedSet.has(person2));
// 输出 Set 的大小
console.log('Set 的大小:', mixedSet.size);
// 遍历 Set 并输出元素
console.log('Set 中的所有元素:');
mixedSet.forEach((element) => {
console.log(element);
});
// 删除元素
mixedSet.delete(person2);
console.log('删除 person2 后 Set 的大小:', mixedSet.size);
// 清空 Set
mixedSet.clear();
console.log('清空 Set 后 Set 的大小:', mixedSet.size);
应用场景
- 数组去重:可以利用
Set的唯一性轻松去除数组中的重复元素。
const arr = [1, 2, 2, 3, 3, 4];
const uniqueArr = [...new Set(arr)];
console.log(uniqueArr);
- 统计唯一元素:在统计页面中出现的唯一元素、用户输入的唯一关键词等场景中非常有用。
const words = ['apple', 'banana', 'apple', 'cherry'];
const uniqueWords = new Set(words);
console.log(uniqueWords.size);
- 交集、并集、差集运算:使用
Set实现集合运算。
const setA = new Set([1, 2, 3]);
const setB = new Set([2, 3, 4]);
// 并集
const union = new Set([...setA, ...setB]);
console.log([...union]);
// 交集
const intersection = new Set([...setA].filter(x => setB.has(x)));
console.log([...intersection]);
// 差集
const difference = new Set([...setA].filter(x => !setB.has(x)));
console.log([...difference]);
Map:存储有序键值对
Map提供了有序的键值对存储。它支持任意类型作为键,且不会对键进行隐式转换。
- 而
Object不支持除了字符串和Symbol类型之外的类型作为键,JavaScript 会自动把其他类型转换为字符串。- 通常,对象会被转换为
'[object Object]',数组会被转换为其元素组成的字符串。
- 通常,对象会被转换为
Map 的遍历顺序与插入顺序一致,这使得在处理需要保持顺序的数据时非常有用。
Object在 ES6 之后的属性遍历顺序虽然有特定规则:整数键会优先按升序排列,非整数键按添加顺序。但这种规则相对复杂,不是简单的插入顺序。
Map可以通过 map.size 属性直接获取键值对的数量。
Object没有直接的方法获取对象的键值对数量,通常使用Object.keys(obj).length间接获取。
Map 的常用方法和属性
-
set(key, value): 向Map中添加一个键值对,如果键已经存在,则更新其值。 -
get(key): 获取Map中指定键的值,如果键不存在,则返回undefined。 -
has(key): 检查Map中是否存在指定的键,返回一个布尔值。例如: -
delete(key): 从Map中删除指定键的键值对,如果删除成功,返回true,否则返回false。 -
clear(): 清空Map中的所有键值对。
// 创建一个新的 Map 对象
const map = new Map();
// 使用 set 方法添加键值对
map.set('name', 'John');
map.set('age', 30);
map.set('city', 'New York');
// 使用 get 方法获取键对应的值
console.log('Name:', map.get('name'));
console.log('Age:', map.get('age'));
// 使用 has 方法检查键是否存在
console.log('Has name:', map.has('name'));
console.log('Has occupation:', map.has('occupation'));
// 使用 delete 方法删除键值对
console.log('Delete name:', map.delete('name'));
console.log('Has name after deletion:', map.has('name'));
// 再次尝试获取已删除的键的值
console.log('Name after deletion:', map.get('name'));
// 使用 clear 方法清空 Map
map.clear();
console.log('Size after clear:', map.size);
-
keys()、values()和entries()- 它们依次返回包含
Map所有键、所有值以及所有键值对的迭代器,方便对Map内容进行遍历操作。
- 它们依次返回包含
const map = new Map();
map.set('name', 'John');
map.set('age', 30);
for (const key of map.keys()) {
console.log(key);
}
// 输出:
// name
// age
for (const value of map.values()) {
console.log(value);
}
// 输出:
// John
// 30
for (const [key, value] of map.entries()) {
console.log(key, value);
}
// 输出:
// name John
// age 30
五、数据遍历:迭代器
迭代器(Iterator)
在 ES6 之前,JavaScript 遍历数据结构的方式较为有限,主要依赖 for 循环、while 循环等,缺乏统一的遍历接口。
什么是迭代器
在 ES6 中,迭代器是实现了迭代器协议的对象。迭代器协议规定,对象必须有 next() 方法,该方法返回一个包含 value 和 done 两个属性的对象。
value:表示当前迭代步骤的值。done:布尔值,true表示迭代结束,false表示还有更多值可迭代。
手动创建迭代器
const myArray = [1, 2, 3];
const myIterator = {
index: 0,
next: function () {
if (this.index < myArray.length) {
return { value: myArray[this.index++], done: false };
} else {
return { value: undefined, done: true };
}
}
};
console.log(myIterator.next()); // { value: 1, done: false }
console.log(myIterator.next()); // { value: 2, done: false }
console.log(myIterator.next()); // { value: 3, done: false }
console.log(myIterator.next()); // { value: undefined, done: true }
示例中,myIterator 是一个自定义的迭代器对象,它通过 next() 方法按顺序返回数组中的元素,直到迭代结束。
自定义可迭代对象
在 ES6 之前,JavaScript 中只有部分内置对象(如数组)能方便地使用 for...of 这类循环进行遍历,而自定义对象没有统一的遍历方式。
ES6 引入了可迭代协议和迭代器协议。
- 可迭代协议允许 JavaScript 对象定义或定制其迭代行为。若一个对象实现了可迭代协议,那么它就成为可迭代对象,能使用
for...of循环、扩展运算符(...)、解构赋值等操作进行遍历。 - 协议规定:一个对象要想成为可迭代对象,必须实现
Symbol.iterator方法。该方法是一个无参的函数,需要返回一个符合迭代器协议的对象。
const myIterable = {
data: [1, 2, 3, 4, 5],
[Symbol.iterator]() {
let index = 0;
return {
next: () => {
if (index < this.data.length) {
return { value: this.data[index++], done: false };
}
return { value: undefined, done: true };
}
};
}
};
// 使用 for...of 循环遍历自定义可迭代对象
for (const item of myIterable) {
console.log(item);
}
内置可迭代对象
ES6 中的许多内置对象,如数组、字符串、Set、Map 等都实现了可迭代协议,这意味着可以直接使用 for...of 循环进行遍历。
// 数组
const arr = [1, 2, 3];
for (const num of arr) {
console.log(num);
}
// 字符串
const str = 'hello';
for (const char of str) {
console.log(char);
}
// Set
const mySet = new Set([1, 2, 3]);
for (const value of mySet) {
console.log(value);
}
// Map
const myMap = new Map([['a', 1], ['b', 2], ['c', 3]]);
for (const [key, value] of myMap) {
console.log(key, value);
}
迭代器的优势
- 统一遍历方式:ES6 的迭代器为不同的数据结构提供了统一的遍历接口,开发者可以使用相同的
for...of语法遍历各种可迭代对象,提高了代码的通用性和可维护性。 - 惰性求值:迭代器支持惰性求值,它不会一次性计算出所有的值,而是在每次调用
next()方法时才计算下一个值,节省了计算资源。
六、增强的对象字面量
ES6 引入的增强对象字面量特性,通过属性简写和方法简写,让对象的定义更加简洁和直观。
属性简写
- 在 ES6 之前,新创建对象的属性名和已有的变量名相同时,就要重复书写变量名来指定属性名和属性值。
- ES6 的属性简写特性允许直接使用变量名作为对象属性名,属性值就是该变量的值。
// ES5 写法
const name = 'Alice';
const age = 25;
const person = {
name: name,
age: age
};
console.log(person); // 输出: { name: 'Alice', age: 25 }
// ES6 写法
const name = 'Alice';
const age = 25;
const person = {
name,
age
};
console.log(person); // 输出: { name: 'Alice', age: 25 }
方法简写
ES6 还提供了方法简写的语法糖,省略了 function 关键字,直接使用方法名和括号定义方法。
// ES5 写法
const personES5 = {
name: 'Alice',
sayHi: function() {
console.log(`Hi, ${this.name}`);
}
};
personES5.sayHi(); // 输出: Hi, Alice
// ES6 写法
const personES6 = {
name: 'Bob',
sayHi() {
console.log(`Hi, ${this.name}`);
}
};
personES6.sayHi(); // 输出: Hi, Bob
七、元编程:Proxy 与 Reflect
元编程是指程序能够对自身进行分析、修改和控制的能力,也就是代码能够操作代码本身。
Proxy:对象操作拦截
Proxy 是 ES6 中引入的一个构造函数,它可以创建一个对象的代理。
- 代理就是对目标对象的一层包装,通过它可以拦截并自定义对目标对象的各种操作。
- 这些操作涵盖了读取属性、设置属性、调用方法、删除属性等常见的对象操作。
- 借助 Proxy,我们可以在运行时动态地控制对象的行为。
定义
Proxy 构造函数接收两个参数:
- 目标对象(target) :这是被代理的原始对象,也就是我们想要对其操作进行拦截和定制的对象。
- 处理程序对象(handler) :该对象包含了一系列的拦截器函数,这些函数用于拦截和自定义目标对象的特定操作,会在相应的对象操作发生时被调用。
- 例如
get用于拦截属性读取操作,set用于拦截属性设置操作等。
- 例如
基本语法:
const proxy = new Proxy(target, handler);
Reflect:对象操作静态方法
Reflect 对象是一个内置的全局对象,它的所有属性和方法都是静态的,即不用通过 new 关键字来创建实例就可直接使用这些方法。
常用方法
Reflect.get(target, propertyKey):获取对象属性值。
const obj = { a: 1 };
console.log(Reflect.get(obj, 'a')); // 输出: 1
Reflect.set(target, propertyKey, value):设置对象属性值,返回操作结果布尔值。
const obj = { a: 1 };
const success = Reflect.set(obj, 'a', 2);
console.log(success); // 输出: true
Reflect.has(target, propertyKey):判断对象是否有某属性,返回布尔值。
const obj = { a: 1 };
console.log(Reflect.has(obj, 'a')); // 输出: true
Reflect.deleteProperty(target, propertyKey):删除对象属性,返回操作结果布尔值。
const obj = { a: 1 };
const deleted = Reflect.deleteProperty(obj, 'a');
console.log(deleted); // 输出: true
八、字符串
字符串新方法
ES6 为字符串新增 includes ()、startsWith ()、endsWith () 等方法。
includes ()方法用于判断字符串是否包含指定子字符串,startsWith ()判断字符串是否以指定子字符串开头,endsWith ()判断字符串是否以指定子字符串结尾。
这些方法在字符串匹配、校验等场景中频繁使用。
const str = 'hello';
str.includes('he'); // true
str.startsWith('h'); // true
str.endsWith('o'); // true
模板字符串
模板字符串是 ES6 引入的一种新的字符串字面量形式,使用反引号(`)包裹。它提供了更简洁的字符串拼接方式,并且可以在字符串中嵌入表达式。
- 基本用法:在模板字符串中,使用
${}包裹表达式,表达式的结果会被插入到字符串中。
const name = 'Alice';
const age = 25;
const message = `My name is ${name} and I'm ${age} years old.`;
console.log(message);
// 输出: My name is Alice and I'm 25 years old.
- 多行字符串:模板字符串可以直接表示多行字符串,不需要像传统字符串那样使用
\n来换行。
javascript
const poem = `
Roses are red,
Violets are blue.
Sugar is sweet,
And so are you.
`;
console.log(poem);
3. 嵌套表达式:模板字符串中的 ${} 可以嵌套使用更复杂的表达式,包括函数调用等。
function getSum(a, b) {
return a + b;
}
const num1 = 5;
const num2 = 3;
const resultMessage = `The sum of ${num1} and ${num2} is ${getSum(num1, num2)}.`;
console.log(resultMessage);
// 输出: The sum of 5 and 3 is 8.
其他实用特性
二进制和八进制字面量
在 ES6 中,支持使用二进制和八进制字面量表示数字,使代码更加清晰直观。
const binary = 0b1010; // 二进制表示
const octal = 0o12; // 八进制表示
二进制字面量以 0b 开头,八进制字面量以 0o 开头,这在处理一些与位运算相关的逻辑或者需要以特定进制表示数字的场景中非常有用。