【前端面试系列】:
- 2024前端面试 --
HTML5+CSS3篇 - 2024前端面试 --
ES6篇 - 2024前端面试 --
JavaScript篇 - 2024前端面试 --
Vue2篇 - 2024前端面试 --
Vue3篇 - 2024前端面试 --
Angular篇 - 2024前端面试 --
Node.js篇 - 2024前端面试 --
Webpack篇
1. ES6 和 ES5的联系、区别、转换等关系
1. 简述 ES6 代码转成 ES5 代码的实现思路是什么?
es6转es5主要通过babel工具实现,Babel是JavaScript编译器,能够将es6转换成向后兼容的es5,从而在现有的浏览器和环境中运行。
主要步骤如下:
- 词法分析:
Babel首先会将输入的代码进行词法分析,将代码分割成一个个 词法单元。 - 语法分析:
Babel会对分割后的词法单元进行 语法分析,生成抽象语法树(AST)。 - 转换:通过对
AST进行遍历和修改,Babel将ES6代码 转换 成ES5代码。 - 代码生成:最后,
Babel会将转换后的AST生成 可运行的ES5代码。
在转换过程中,
Babel会根据预定义的插件和预设对代码进行转换。插件和预设可以分别处理一些特定的语法和功能,如箭头函数、类和模块等。同时,Babel还支持开发者自定义插件和预设来处理更加特殊的个性化的需求。
- 语法转换:本质是将
ES6语法转成AST,再将AST转为ES5语法代码。- 例如:将
let、const转换成var,箭头函数转换成Function函数声明等。
- 例如:将
API转换:采用Babel-polyfill等工具对ES5中不存在的API(包括Set等ES6中新的数据结构)做修复。- 例如:
Array.prototype.includes、Set、Map等在ES5中不存在,需要用响应的ES5代码实现这些API。
- 例如:
综上所述,
ES6代码转成ES5代码的实现思路涉及词法分析、语法分析、AST的生成和转换、以及使用插件和预设进行特定语法的处理,同事还需要考虑API地兼容性问题。
1.1 解释 babel 是什么?有什么作用?
Babel是一个JavaScript编译器,它可以将ECMAScript 2015+版本的代码转换成向后兼容的JavaScript代码,以便在现有的浏览器中运行。Babel可以帮助开发者使用最新的JavaScript语言特性,而不用担心浏览器兼容性问题。
Babel转换代码。这些特性主要包括箭头函数、解构赋值、模版字符串、let和const等等。还支持转换JSX语法,使得React的代码可以在浏览器中运行。
Babel可以使用插件来扩展其功能。例如,可以将TypeScript转换成JavaScript代码,或者使用插件来判断新的ECMAScript特性。Babel还可以与许多构建工具(如webpack、Rollup等)集成,以便在构建过程中自动转换代码。
2. ES5、ES6(ES2015)有什么区别?
ES5:指的是ECMAScriot的第五个版本,发布于2009年,是目前最广泛使用的JavaScript版本。ES6:指的是ECMAScriot的第六个版本,也称为ES2015,发布于2015年,引入了许多新的语言特性和语法糖。
ES6相较于ES5的主要区别包括:
- 新的语法特性,如箭头函数、类、模版字符串、解构赋值等;
- 新的数据类型,如
Set、Map、Symbol等;- 新的迭代器和生成器,使得处理数据集合更加方便;
- 新的模块化系统,使得代码的组织和管理更加容易;
- 新的
Promise函数,使得异步编程更加简单可读;- 新的默认参数和剩余参数语法,使得函数的定义和调用更多灵活。
ES6新增的特性:
let、const定义块级作用域- 箭头函数
- 解构赋值
- 扩展运算符
- 常见的数组的方法、伪数组
- 模版字符串
class类- 参数设置默认值
promisefor...in,for...of
3. 详细描述 ES6 和 ECMAScript 2015 的关系?
ES2015 是 ES6 的官方名称,但是由于 ES6 引入了太多的新特性,因此人们通常使用 ES2015 代替 ES6.
4. 简述 ES5/ES6 的继承除了写法以外还有什么区别?
ES5/ES6 除了继承除了写法外,主要还有继承的机制和类的方法属性。
4.1. 继承机制:
ES5的继承主要通过原型链和构造函数来实现。- 可以通过
Parent.apply(this, arguments)将父类的属性和方法添加到子类的实例上,这种方法相对复杂且不够直观。 - 这种方式下,子类的示例创建后,通过调用父类的构造函数来初始化父类的部分属性。
- 可以通过
ES6引入了class关键字,使得继承的写法更加简洁和直观。- 子类的构造函数必须首先调用
super()来获取父类的属性和方法,然后再添加或修改子类特有的属性和方法。 - 在这种方式下,子类的实例创建基于父类实例,只能通过
super()才能访问父类的属性和方法。
- 子类的构造函数必须首先调用
// ES5
// 定义父类
function Parent(value) {
this.language = ["js", "less", "react"];
this.value = value;
}
// 定义子类
function child() {
Parent.apply(this, arguments);
}
const test = new Child(888);
test.language; // ["js", "less", "react"]
test.value; // 888
// ES6
class Child2 extends Parent2 {
constructor(props) {
super(props);
}
}
var test2 = new Child2("super data");
test2.value; // super data
4.2. 类的方法和属性
ES5中,通过原型链添加的方法和属性是可枚举的。例如,通过Object.keys(Bar)可以获取到Bar上的所有可枚举数据类型,包括实例方法和静态方法。ES6中,类的所有方法(包括静态方法和实例方法)都是不可枚举的。这意味着使用Object.keys(Foo)无法获取到Foo类上的任何方法或属性,包括其原型上的方法。
5. 简述 ES6 之前使用 prototype 实现继承?
在引入 class 之前,JavaScript 使用 prototype 来实现继承。每个函数都有一个 prototype 属性,这个属性指向一个对象,而这个对象被用作通过该构造函数创建的所有对象的原型。通过修改这个原型对象,可以添加属性和方法,这些属性和方法会被所有实例共享。
// 父类构造函数
function Parent(name) {
this.name = name;
}
// 父类方法
Parent.prototype.greet = function () {
console.log("Hello, my name is" + this.name);
};
// 子类构造函数
function Child(name, age) {
Parent.call(this, name); // 调用父类构造函数给子类实例设置name属性
this.age = age;
}
// 创建一个Parent实例,将其方法赋予Children.prototype
Child.prototype = Object.create(Parent.prototype);
// 修正Child.prototype的构造器指向
Child.prototype.constructor = Child;
// 子类持有方法
Child.prototype.introduce = function () {
console.log("My name is " + this.name + ", I am " + this.age);
};
// 测试继承
var childInstance = new Child("Alice", 22);
childInstance.greet(); // Hello, my name isAlice
childInstance.introduce(); // My name is Alice, I am 22
6. 简述怎样通过 ES5 及 ES6 声明一个类?
ES5中,类的创建主要通过构造函数来实现,构造函数本身可以看作是一种特殊的函数,用于创建和初始化对象。
// 创建构造函数
let Animal = function (type) {
this.type = type;
};
// 在构造函数的原型上添加共享的方法,这样所有通过该构造函数创建的对象都会继承这些方法。
Animal.prototype.walk = function () {
console.log("I am walking");
};
new Animal("dog");
new Animal("monkey");
// 这样,通过 `new Animal("dog")` 创建的对象 `dog`和通过 `new Animal("monkey")`创建的对象 `monkey`都会继承`walk`方法。
ES6中,类的声明变得更加直观和简洁。ES6引入了class关键字,允许开发者以更接近传统面向对象编程的方式来声明类
// 声明类
class Animal {
// consttuctor:一个特殊的方法,用于创建对象时初始化对象的属性。
constructor(type) {
this.type = type;
}
// 其他的方法,则是类的成员方法,所有该类的实例都会继承这些方法。
walk() {
console.log("I am walking");
}
}
// 语法简洁,更符合传统的面向对象编程思维,代码更易于理解和维护。
2. ES6 的升级
1. 简述 ES6 let 有什么用?有了 var 为什么还要用 let?
在 ES6 之前,声明变量只能用 var,var 方式声明变量其实是很不合理的,准确的说,是因为 ES5 里面没有块级作用域是很不合理的,甚至可以说是一个语言层名的 bug。没有块级作用域会带来很多难以理解的问题,比如 for循环 var 变量泄露,变量覆盖等问题,let 声明的变量拥有自己的块级作用域,且修复了 var 声明变量带来的变量提升问题。
{
var i = 10;
}
console.log(i); // 10
{
let j = 10;
}
console.log(j); // Uncaught ReferenceError: j is not defined
let在for循环中使用:每次循环都会创建一个新的且独立的块级作用域,使用let声明变量传入到for循环体的作用域中,不会改变,不会受到外界的影响。
// var:
var a = [];
for (var i = 0; i < 3; i++) {
a[i] = function () {
console.log(i);
};
}
a[0](); // 3
a[1](); // 3
a[2](); // 3
// 结果:不是预期的0,1,2
// 解释:因为使用 var 声明变量,i 是全局变量,每次循环,都会修改 i 的值,相当于给 i 重新赋值,因此最后输出的 i 是 for循环 完成后的值,因此输入结果全是 3
// var换成let后:
var b = [];
for (let j = 0; j < 3; j++) {
b[j] = function () {
console.log(j);
};
}
b[0](); // 0
b[1](); // 1
b[2](); // 2
let没有变量提升- 变量提升:用
let声明不存在的变量提升,必须等let执行完毕之后,变量才可以使用,否则会报错Uncaught ReferenceError
console.log(c); // Uncaught ReferenceError: Cannot access 'c' before initialization
let c = "c";
// var 存在变量提升,无论是开头还是结尾使用var声明都可以使用。
console.log(d); // undefined
var d = "d";
let同一个作用域不可重复声明,var则不会报错
let a = 1;
let e = 2; // Uncaught SyntaxError: Identifier 'e' has already been declared
var a = 1;
var a = 2;
2. 简述 ES6 对 String 字符串类型做的常用升级优化?
- 模版字符串(
Template Literals):使用反引号( ` )来定义字符串,可以内嵌变量和表达式,还能包含换行符。
let name = "Name";
let greeting = `Hello, ${name}!`;
console.log(greeting); // Hello, Name!
- 字符串扩展:包含了多个新的方法,如
includes(),startsWith(),endsWith(), 用于判断字符串是否包含、是否以特定子串开始或结束。
let test = "hello world!";
console.log(test.startsWith("hello")); // true;
console.log(test.endsWith("world!")); // true;
console.log(test.includes("world")); // true;
- 重复字符串:新增了字符串的重复功能,可以用
repeat(n)来生成一个新的字符串,其中包含原始字符串n次。
let str = "abc";
let repeated = str.repeat(3);
console.log(repeated); // abcabcabc
- 字符串迭代器:
String类型现在可以用for...of循环来进行迭代,每次迭代的是字符串中的没一个字符。
for (const char of "hello") {
console.log(char);
}
// h
// e
// l
// l
// o
3. 简述 ES6 对 Array 数组类型做的常用升级优化?
- 解构赋值:可以直接从数组中提取值并赋值给变量。
let [a, b, c] = [1, 2, 3];
// a=1,b=2,c=3
- 扩展运算符(
...):用于取出参数对象的所有可遍历属性,拷贝到当前对象之中。
let arr1 = [1, 2, 3];
let arr2 = [...arr1]; // [1, 2, 3]
Array.from():可以将类似数组的对象和可遍历对象转换为数组。
let arr3 = { 0: "a", 1: "b", 2: "c", length: 3 };
let arr4 = Array.from(arr3); // ['a', 'b', 'c']
- 数组的
map()和filter()方法:这两个方法可以用于对数组进行更复杂的操作,map()用于创建新数组,filter()用于过滤新数组。
let numbers = [1, 2, 3, 4, 5];
let doubled = numbers.map((number) => number * 2); // [2, 4, 6, 8, 10]
let even = numbers.filter((num) => num % 2 === 0); // [2, 4]
- 简写属性名:在对象字面量中,可以在属性名与变量名相同时使用简写。
let a = 1,
b = 2,
c = 3;
let arr = [a, b, c];
console.log(`output->obj`, arr); // [1, 2, 3]
rest参数(...):用户获取函数的多余参数,这些参数以数组形式表示。
function add(...values) {
let sum = 0;
for (let val of values) {
sum += val;
}
return sum;
}
console.log(add(1, 2, 3)); // 6
- 迭代器与生成器:
ES6中引入了新的for...of循环,用于迭代数据集合,比如Arrays,Maps,Sets等
for (let value of ["a", "b", "c"]) {
console.log(value);
}
// a
// b
// c
Promise与异步编程:ES6引入了Promise对象,用于更优雅地处理异步编程。
let promise = new Promise((resolve, reject) => {
setTimeout(() => resolve("done!"), 1000);
});
promise.then(alert); // 1秒后弹窗显示 "done!"
4. 简述 ES6 对 Number 类型做的常用升级优化?
- 指数运算符: 使用
\*\*代替Math.pow()来进行指数运算。
// 指数运算符
let x = 2;
let y = 3;
let result = x ** y; // 结果为8
Number.isFinite(): 用于检查一个数值是否为有限数。
// Number.isFinite()
console.log(Number.isFinite(0.1)); // 输出:true
console.log(Number.isFinite(Infinity)); // 输出:false
Number.isNaN(): 用于检查一个值是否为NaN。
// Number.isNaN()
console.log(Number.isNaN(NaN)); // 输出:true
Number.parseInt()和Number.parseFloat(): 作为静态方法,可以直接调用,而不用将它们绑定到Number.prototype。
// Number.parseInt()
let num = Number.parseInt("123", 10); // 输出:123
Number.isInteger(): 判断一个值是否为整数。
// Number.isInteger()
console.log(Number.isInteger(123)); // 输出:true
- 安全整数:
Number.MIN_SAFE_INTEGER和Number.MAX_SAFE_INTEGER标识JavaScript中最小和最大安全整数。
// 安全整数
console.log(Number.MIN_SAFE_INTEGER); // -9007199254740991
console.log(Number.MAX_SAFE_INTEGER); // 9007199254740991
5. 简述 ES6 对 Object 对象类型做的常用升级优化【重要】?
- 属性的简写
let name = "Alice";
let age = 29;
const person = { name, age };
- 属性名可以使用表达式
let key = "name";
const person = { [key]: "Alick", age: "23" };
- 方法的简写
const person = {
name: "Alice",
greet() {
return `Hello, my name is ${this.name}!`;
},
};
- 新的方法
// Object.is()
console.log(Object.is("foo", "foo")); // true
console.log(Object.is({}, {})); // false,因为对象比较是引用比较
// Object.assign()
const object1 = { a: 1 };
const object2 = { b: 2 };
const object3 = { c: 3 };
const mergedObject = Object.assign(object1, object2, object3);
// mergedObject 现在有属性 a, b, 和 c
6. 简述 ES6 对 Function 函数类型做的常用升级优化?
- 箭头函数:简化了函数的定义的方式,用关键字
=>代替function
// ES5
var sum = function (a, b) {
return a + b;
};
// ES6
const sum = (a, b) => a + b;
- 函数参数默认值:允许给函数参数指定默认值,避免了在函数体内部进行判断
// ES5
function multiply(a, b) {
b = b || 1;
return a * b;
}
// ES6
function multiply(a, b = 1) {
return a * b;
}
- rest 参数:允许函数接收数组或是可迭代对象中的多个参数
// ES5
function sum(arr) {
return arr.reduce((a, b) => a + b, 0);
}
// ES6
function sum(...numbers) {
return numbers.reduce((a, b) => a + b, 0);
}
- 扩展运算符:允许一个可迭代的对象展开为函数参数
// ES5
var numbers = [1, 2, 3, 4, 5];
Math.max.apply(null, numbers);
// ES6
const numbers = [1, 2, 3, 4, 5];
Math.max(...numbers);
- 对象方法:允许在对象字面量中使用简写方法定义
// ES5
var obj = {
value: 1,
double: function () {
return this.value * 2;
},
};
// ES6
const obj = {
value: 1,
double() {
return this.value * 2;
},
};
- 函数
name属性: 函数现在有了一个标准化的名字
function foo() {}
console.log(foo.name); // "foo"
- 尾调用优化(Tail Call Optimization): 允许函数的最后一个操作是返回一个函数的递归调用或者一个构造函数的调用
// ES5
function factorial(n) {
if (n === 1) return 1;
return n * factorial(n - 1);
}
// ES6
function factorial(n) {
if (n === 1) return 1;
// 尾调用
return n * factorial(n - 1);
}
7. 简述 ES6 Symbol 的作用?
Symbol 是 ES6 引入的一种新的原始数据类型。它是唯一且不可变的数据类型。主要用于创建对象的唯一属性名,以解决命名冲突的问题,以及为对象添加独一无二的属性,这些属性不会与其他属性键冲突。
Symbol 的主要作用和特性包括:
- 唯一性:每个通过
Symbol()函数创建的symbol值都是唯一的,即使是用相同的参数创建的symbol也不相等。这保证了使用symbol作为对象属性名时,不会与其他属性名发生冲突。 - 不可变性:
symbol一旦被创建,就不能被修改,他们是不可变的确保了属性名的稳定性。 - 使用场景:
- 私有属性:
Symbol常被用来作为对象的私有成员,因为Symbol类型的属性不会出现在常规的对象属性枚举中,例如for...in循环或Object.keys()方法中,这使得symbol属性可以被视为对象的私有属性。 - 防止命名冲突:在大型项目或者是多人协作的项目中,使用
symbol可以防止属性名的冲突,特别是扩展第三方库的对象时尤其重要。 - 使用
Well-known Symbols来实现对象接口:ES6定义了一些内置的well-known symbols,他们通过Symbol构造函数的静态属性访问,如Symbol.iterator,Symbol.asyncIterator,Symbol.toStringTag等。这些Symbols用于实现对象的标准行为,例如定义迭代器、异步迭代器或改变对象的字符串描述等。
- 私有属性:
创建 Symbol
let sym1 = Symbol();
let sym2 = Symbol("desc");
let sym3 = Symbol("desc");
console.log(sym2 === sym3); // false
使用 Symbol 作为对象的属性名
let mySymbol = Symbol();
let obj = {
[mySymbol]: "value",
};
console.log(obj[mySymbol]); // "value"
Symbol 保证属性不会被意外覆盖或枚举
let id = Symbol("id");
let person = {
name: "John",
age: 20,
[id]: 111,
};
for (let key in person) {
console.log(key); // name,age
}
console.log(Object.keys(person)); // ["name","age"]
console.log(person[id]); // 111
8. 简述 ES6 Set 的作用?
ES6 引入 Set 新的数据结构,其主要作用是提供一种存储唯一值的集合,无论这个值是原始值还是对象引用,Set 对象允许存储任何类型的唯一值,无论是原始值还是对象引用,他们在 Set 种不会重复出现。
Set 的主要特性和作用包括:
- 唯一性:
Set内部的值都是唯一的,这意味着Set集合中没有重复的值,这对于需要元素唯一性的数据结构非常有用。例如:去重 - 值的类型:
Set可以存储任何类型的值,包括原始类型和对象引用。 - 数据操作:
Set提供了简单的操作方法,包括add(value)添加新元素,delete(value)删除元素,has(value)检查元素是否存在,以及clear()清空所有元素。这些方法提高了数据操作的便利性。 - 迭代方法:
Set是可迭代的,它提供了forEach方法及keys,values,entries迭代器方法,使得遍历集合变得非常简单。由于Set的值是唯一的,所以keys()和values()方法的行为是相同的。 - 集合大小:通过
size属性,可以很方便的获取集合中元素的数量。
创建 Set 并添加元素
let mySet = new Set();
mySet.add(1);
mySet.add("some text");
mySet.add({ a: 1, b: 2 });
console.log(mySet.size); // 3
检查值是否在 Set 中
console.log(mySet.has(1)); // true
console.log(mySet.has(3)); // false
遍历 Set
mySet.forEach((value) => {
console.log(value);
});
// 1
// some text
// {a: 1, b: 2}
使用 Set 去重
const numbers = [2, 3, 4, 5, 2, 3, 8];
const uniqueNumbers = new Set(numbers);
console.log(uniqueNumbers); // Set(5) {2, 3, 4, 5, 8}
9. 简述 ES6 Map 的作用?
ES6 引入 Map 对象作为一种新的键值对集合结构,它提供了比传统对象字面量更灵活和强大的方式来存储数据。Map 对象可以使用任何类型的值(包括对象)作为键,这里它与传统对象最大的不同之处,后者仅支持字符串和 Symbol 作为键名。
Map 的主要特性和作用:
- 键的多样性:在
Map中,键可以是任意类型的值,包括函数、对象或任何原始类型。 - 元素顺序:
Map对象维护键值对的插入顺序,当进行迭代时,会按照元素的插入顺序返回键值对。 - 大小可测:通过
Map的size属性可以直接获取集合的大小,这比传统对象需要手动计数更为方便。 - 性能优化:对于频繁增删键值对的场景,
Map的性能通常优于传统的对象,因为Map是专门为了大量数据的存储而设计的。 - 更好的迭代器支持:
Map对象是可迭代的,它提供了forEach方法以及keys(),values(),entries()这些迭代器方法,使得遍历数据变得非常简单。 - 直接数据操作方法:
Map提供了set(key, value),get(key),has(key),delete(key)和clear等方法,用于更加直接和便捷的操作数据。
创建 Map 并添加元素:
let myMap = new Map();
myMap.set("key1", "value1");
myMap.set("1", "value2");
myMap.set({}, "value3");
console.log(myMap); // Map(3) {'key1' => 'value1', '1' => 'value2', {…} => 'value3'}
console.log(myMap.size); // 3
获取和设置值:
console.log(myMap.get("key1")); // value1
console.log(myMap.get(1)); // value2
console.log(myMap.get({})); // undefined,因为{}是一个新的对象引用
遍历 Map:
myMap.forEach((value, key) => {
console.log(key, value);
});
// key1 value1
// 1 'value2'
// Object {} 'value3'
使用 Map 进行数据结构的优化:
Map 的引入使得 JavaScript 在处理复杂的数据结构时更加灵活和强大,尤其是在需要键值对存储且键为非字符串时。此外,Map 的性能优化和迭代器支持使得数据操作和遍历更为高效和方便。