最近找工作整理了一些面试题,分享给大家一起来学习。如有问题,欢迎指正。
前端面试题系列文章:
ECMAScript 和 JavaScript 的关系
ECMAScript是JavaScript的规格,JavaScript是ECMAScript的一种实现。 通常看做JavaScript的标准化规范。
let、const、var的区别
- 作用域:let,const块作用域由
{ }
包括,var全局作用域(window属性)和函数作用域。 - 变量提升:var存在变量提升,let和const不存在变量提升,即在变量只能在声明之后使用,否在会报错。
- 暂时性死区:在使用let、const命令声明变量之前,该变量都是不可用的。这在语法上,称为暂时性死区。使用var声明的变量不存在暂时性死区。
- 重复声明:var声明变量时,可以重复声明变量,后声明的同名变量会覆盖之前声明的变量。const和let不允许重复声明变量。
- 初始值设置:var 和 let 声明变量可以不用设置初始值。而const声明变量必须设置初始值。
- 修改变量值:let声明的变量是可以重新赋值。const声明的常量的值就不能改变(指向的内存地址不变)。
区别 | var | let | const |
---|---|---|---|
是否有块级作用域 | × | ✔️ | ✔️ |
是否存在变量提升 | ✔️ | × | × |
是否添加全局属性 | ✔️ | × | × |
能否重复声明变量 | ✔️ | × | × |
是否存在暂时性死区 | × | ✔️ | ✔️ |
是否必须设置初始值 | × | × | ✔️ |
能否改变指针指向 | ✔️ | ✔️ | × |
扩展运算符
- 数组
console.log(...[1, 2, 3]);
console.log(1, ...[2, 3, 4], 5); // 1 2 3 4 5
// ES5 的写法
Math.max.apply(null, [14, 3, 77])
// ES6 的写法
Math.max(...[14, 3, 77])
// 等同于
Math.max(14, 3, 77);
- 对象
let { x, y, ...z } = { x: 1, y: 2, a: 3, b: 4 };
x; // 1
y; // 2
z; // { a: 3, b: 4 }
解构赋值
- 默认值结构赋值
let { first: f, last: l } = { first: 'hello', last: 'world' }; // f='hello'; l='world' let {x = 3} = {}; // x=3 let {x, y = 5} = {x: 1}; // x=1; y=5 let {x = 3} = {x: undefined}; // x=undefined let {x = 3} = {x: null}; // x=3 let [x = 1, y = x] = []; // x=1; y=1 let [x = 1, y = x] = [2]; // x=2; y=2 let [x = 1, y = x] = [1, 2]; // x=1; y=2 let [x = y, y = 1] = []; // ReferenceError: y is not defined
- 字符串的解构赋值
const [a, b, c, d, e] = 'hello'; a // "h" b // "e" c // "l" d // "l" e // "o" let {length : len} = 'hello'; len // 5
- 扩展运算符可以与解构赋值结合起来
const [first, ...rest] = [1, 2, 3, 4, 5]; first // 1 rest // [2, 3, 4, 5] const [first, ...rest] = []; first // undefined rest // [] const [first, ...rest] = ["foo"]; first // "foo" rest // []
字符串扩展
- 字符串添加了遍历器接口,使得字符串可以被
for...of
循环遍历for (let codePoint of 'foo') { console.log(codePoint) } // "f" // "o" // "o"
- 模板字符串,用反引号(`)标识
- 新增字符串实例方法
-
includes:判断是否包含子字符串
const son = 'haha' const father = 'xixi haha hehe' father.includes(son) // true
-
startsWith:判断是否以某个字符串开头
const father = 'xixi haha hehe' father.startsWith('haha') // false father.startsWith('xixi') // true
-
endsWidth:判断是否以某个字符串结尾
const father = 'xixi haha hehe' father.endsWith('hehe') // true
-
repeat:重复多次某个字符串
const sourceCode = 'repeat for 3 times;' const repeated = sourceCode.repeat(3) console.log(repeated) // repeat for 3 times;repeat for 3 times;repeat for 3 times;
-
trimStart(),trimEnd(): 去除字符串头部|尾部空格
const s = ' abc '; s.trim() // "abc" s.trimStart() // "abc " s.trimEnd() // " abc"
-
数值扩展
- BigInt数据类型: 用来表示整数,没有位数的限制,任何位数的整数都可以精确表示。
typeof 1234; // number typeof 1234n; // bigint typeof BigInt(123); // bigint 1n + 2n; // 3n
- 指数运算符(**)
2**2 // 4 2**3 // 8
- Number对象上新增方法
- Number.isFinite():用来检查一个数值是否为有限的(finite),即不是
Infinity
。 - Number.isNaN():用来检查一个值是否为
NaN
。 - Number.parseInt():把值转化为整数
- Number.parseFloat():把值转化为浮点数值
- Number.isInteger():用来判断一个数值是否为整数
- Number.isFinite():用来检查一个数值是否为有限的(finite),即不是
- Math对象上新增方法
- Math.trunc():用于去除一个数的小数部分,返回整数部分
- Math.sign():用来判断一个数到底是正数、负数、还是零、其他值
- Math.cbrt():用于计算一个数的立方根
数组扩展
- Array对象新增方法
- Array.from():将类似数组的对象和可遍历(iterable)的对象(包括 ES6 新增的数据结构 Set 和 Map),转为数组
// 对象 let arrayLike = { 0: "a", 1: "b", 2: "c", length: 3, }; let arr2 = Array.from(arrayLike); // ['a', 'b', 'c'] Array.from({ length: 3 }); // [ undefined, undefined, undefined ] // arguments对象 function foo() { var args = Array.from(arguments); // ... } // 字符串 Array.from("hello"); // ['h', 'e', 'l', 'l', 'o'] // Set Array.from(new Set([1, 2, 3, 4, 4])); // [1, 2, 3, 4] // Map const m = new Map(); const o = { a: "a" }; m.set(o, "2"); m.set(1, o); Array.from(m); // [[{ a: "a" }, "2"], [1, { a: "a" }]]
- Array.of():方法用于将一组值,转换为数组
Array.of(3, 11, 8) // [3,11,8] Array.of(3) // [3] Array.of(3).length // 1
- 新增数组实例方法
- find() 和 findIndex()
- entries(),keys() 和 values()
- includes()
对象的扩展
- 函数的
name
属性,返回函数名 - 属性的遍历
- for…in:只遍历对象自身的和继承的可枚举的属性(不含 Symbol 属性)。一般搭配obj.hasOwnProperty(key)方法判断是否对象自身属性。
- Object.keys(obj):返回一个数组,包括对象自身的(不含继承的)所有可枚举属性(不含 Symbol 属性)的键名。
- Object对象新增方法
- Object.is():比较两个值是否相等,与===类似,区别如下:
+0 === -0 //true NaN === NaN // false Object.is(+0, -0) // false Object.is(NaN, NaN) // true
- Object.assign():用于对象的合并
const target = { a: 1 }; const source1 = { b: 2 }; const source2 = { c: 3 }; Object.assign(target, source1, source2); target // {a:1, b:2, c:3}
- Object.keys(),Object.values(),Object.entries()
const obj = { foo: '41', baz: 42 }; Object.keys(obj) // ["foo", "baz"] Object.values(obj) // ["41", 42] Object.entries(obj) // [ ["foo", "41"], ["baz", 42] ]
函数的扩展
- 参数默认值
- rest 参数(形式为...变量名)
- 箭头函数
Set 和 Map 数据结构
-
Set: 本身是一个构造函数,用来生成 Set 数据结构。类似于数组,但是成员的值都是唯一的,没有重复的值。
const set = new Set([1, 2, 3, 4, 4]); [...set] // [1, 2, 3, 4] set.size // 4 set.add(5) set.has(2) // true set.delete(2) set.has(2) // false
使用Set去重
// 去除数组的重复成员 [...new Set(array)] // 字符串去重 [...new Set('ababbc')].join('') // "abc"
Set 结构转为数组
const items = new Set([1, 2, 3, 4, 5]); const array = Array.from(items); const array1 = [...items];
遍历方法 keys(),values(),entries(),for...of,forEach()
let set = new Set(["red", "green", "blue"]); for (let item of set.keys()) { console.log(item); } // red // green // blue for (let item of set.values()) { console.log(item); } // red // green // blue for (let item of set.entries()) { console.log(item); } // ["red", "red"] // ["green", "green"] // ["blue", "blue"] for (let x of set) { console.log(x); } // red // green // blue set.forEach((value, key) => console.log(key + " : " + value)); // red : red // green : green // blue : blue
-
Map 本身是一个构造函数, 用来生成Map数据结构。它类似于对象,也是键值对的集合,但是“键”的范围不限于字符串,各种类型的值(包括对象)都可以当作键
const m = new Map(); const o = { a: "a" }; m.set(o, "2"); m.set(1, o); m.has(1); // true m.has("2"); // false m.has(o); // true m.get(o); // "2" m.get(1); // {a: 'a'} m.delete(1) m.has(1); // false m.size; // 1
遍历方法 keys(),values(),entries(),for...of,forEach()
const map = new Map([ ["F", "no"], ["T", "yes"], ]); for (let key of map.keys()) { console.log(key); } // "F" // "T" for (let value of map.values()) { console.log(value); } // "no" // "yes" for (let item of map.entries()) { console.log(item[0], item[1]); } // "F" "no" // "T" "yes" for (let [key, value] of map.entries()) { console.log(key, value); } // "F" "no" // "T" "yes" // 等同于使用map.entries() for (let [key, value] of map) { console.log(key, value); } // "F" "no" // "T" "yes"
Symbo的理解和使用
一种新的原始数据类型Symbol
,表示独一无二的值
let s1 = Symbol()
let s2 = Symbol()
let s3 = Symbol('another symbol')
s1 === s2 // false
typeof s1 // 'symbol'
应用场景:
-
使用Symbol来作为对象属性名(key)
Symbol类型的key不能通过
Object.keys()
或者for...in
来枚举,且使用JSON.stringify()
转换会丢失。所以,利用该特性,我们可以把一些不需要对外操作和访问的属性使用Symbol来定义。const name = Symbol('name') let obj = { [name]: 'Lily', age: 18, title: 'Engineer' } Object.keys(obj) // ['age', 'title'] for (let p in obj) { console.log(p) // 分别会输出:'age' 和 'title' } JSON.stringify(obj) // {"age":18,"title":"Engineer"} Object.getOwnPropertySymbols(obj) // [Symbol(name)]
-
使用Symbol来替代常量
当需要多个常量来进行业务逻辑判断处理时,使用Symbol可保证唯一性
const TYPE_AUDIO = Symbol() const TYPE_VIDEO = Symbol() const TYPE_IMAGE = Symbol()
-
使用Symbol定义类的私有属性/方法
const PASSWORD = Symbol() class Login { constructor(username, password) { this.username = username this[PASSWORD] = password } checkPassword(pwd) { return this[PASSWORD] === pwd } } const login = new Login('admin', '123456') login.checkPassword('123456') // true
Proxy 的理解和使用
用于创建一个对象的代理,从而实现基本操作的拦截和自定义(如属性查找、赋值、枚举、函数调用等)
let proxy = new Proxy(target, handler);
target
表示所要拦截的目标对象(任何类型的对象,包括原生数组,函数,甚至另一个代理))
handler
参数也是一个对象,用来定制拦截行为。
-
用法
let obj = new Proxy({}, { get: function (target, propKey, receiver) { console.log(`获取对象属性时执行`); return Reflect.get(target, propKey, receiver); }, set: function (target, propKey, value, receiver) { console.log(`设置对象属性时执行`); return Reflect.set(target, propKey, value, receiver); }, });
Reflect
方法,保证原生行为能够正常执行 -
使用场景
- 拦截和监视外部对对象的访问
- 降低函数或类的复杂度
- 在复杂操作前对操作进行校验或对所需资源进行管理
Promise 的理解和使用
Promise 是异步编程的一种解决方案。解决了地狱回调的问题。
-
状态
pending
(进行中)fulfilled
(已成功)rejected
(已失败)
-
特点
- 对象的状态不受外界影响
- 一旦状态改变(从
pending
变为fulfilled
和从pending
变为rejected
),就不会再变,任何时候都可以得到这个结果
-
用法
const promise = new Promise(function (resolve, reject) { setTimeout(() => { if ("判断执行成功") { resolve("success"); } else { reject("error"); } }, 50); }); promise .then( (result) => { console.log("resolved状态的回调执行"); }, (error) => { console.log("rejected状态的回调执行"); } ) .catch((error) => { console.log("rejected状态的回调执行 或 捕获then中抛出错误"); }) .finally(() => { console.log("没有入参,任何状态都执行"); });
-
常用API
Promise.all()
返回新的Promise对象。传入多个Promise,所有状态都为fulfilled时,当前Promise对象状态为fulfilled。或者只要有一个状态为rejected,当前Promise对象状态为rejected。Promise.allSettled()
返回新的Promise对象,传入多个Promise,所有状态都发生改变(fulfilled|rejected),当前Promise对象状态为fulfilled。Promise.race()
返回新的Promise对象,传入多个Promise,只要有一个状态改变时(fulfilled|rejected),当前Promise对象状态就改变(fulfilled|rejected)Promise.any()
返回新的Promise对象,传入多个Promise,只要有一个状态为fulfilled时,当前Promise对象状态为fulfilled
-
使用场景
- 链式操作
new Promise((resolve, reject) => { resolve(1); }) .then((res) => { return res + 1; }) .then((res) => { return res + 1; }) .then((res) => { console.log(res + 1); // 4 });
- Promise.all()实现发送请求获取多个数据后执行某个操作。
const p1 = new Promise((resolve, reject) => { resolve(1); }); const p2 = new Promise((resolve, reject) => { resolve(2); }); Promise.all([p1, p2]).then((res) => { console.log(res); // [1, 2] });
-
简易实现Promise
function MyPromise(fn) { let status = "pending"; let fulfilledsCallbackArr = []; let rejectedCallbackArr = []; let resolve = (res) => { if (status === "pending") { status = "fulfilled"; fulfilledsCallbackArr.forEach((f) => f(res)); } }; let reject = (res) => { if (status === "pending") { status = "rejected"; rejectedCallbackArr.forEach((f) => f(res)); } }; this.then = function (f1, f2) { if (status === "pending") { return new MyPromise((rel, rej) => { fulfilledsCallbackArr.push((res) => { rel(f1(res)); }); rejectedCallbackArr.push((res) => { rej(f2(res)); }); }); } else { return new MyPromise((rel, rej) => { if (status === "fulfilled") { rel(f1(res)); } if (status === "fulfilled") { rej(f2(res)); } }); } }; this.catch = function (f) { rejectedCallbackArr.push(f); }; fn(resolve, reject); }
Generator 的理解和使用
Generator:(生成器)是ES6标准引入的新的数据类型。generator由function*
定义,除了return
语句,还可以用yield
返回多次
function* generator() {
yield 1;
yield 2;
}
const gen = generator();
console.log(gen.next()); // {value: 1, done: false}
console.log(gen.next()); // {value: 1, done: false}
console.log(gen.next()); // {value: undefined, done: false}
function* foo(x) {
console.log("hi" + (yield x + 1))
yield x + 2
return x + 3;
}
const f = foo(1);
console.log(f.next()); // { value: 2, done: false }
console.log(f.next()); // hiundefined // { value: 3, done: false }
console.log(f.next()); // { value: 4, done: true }
console.log(f.next()); // {value: undefined, done: true}
next()
方法会执行generator的代码,然后,每次遇到yield x;
就返回一个对象{value: x, done: true/false}
,然后“暂停”。返回的value
就是yield
的返回值,done
表示这个generator是否已经执行结束了。
yield表达式可以暂停函数执行,next方法用于恢复函数执行,这使得Generator函数非常适合将异步任务同步化
。
const request = (id) => {
setTimeout(() => {
const obj = {
id:id
}
it.next(obj)
}, 1000)
}
function* gen(mydata) {
const data = yield request(mydata);
const data1 = yield request(data.id);
const data2 = yield request(data1.id);
console.log('Generator:' + data3.id);
}
const it = gen("123")
it.next()
js模块化加载规范
-
什么是模块化
将一个复杂的程序依照一定的规则封装成几个文件, 并进行组合在一起。每个文件模块的内部数据是私有的, 只是向外部暴露一些接口(方法)与外部其它模块通信。
-
为什么需要模块化(模块化优点)?
- 避免全局作用域污染
- 提高代码复用
- 提高代码可维护性
- 更好的分离,实现按需加载
-
模块化规范
- AMD
:浏览器端的模块加载规范
。代表库有RequireJS - CMD:
浏览器端的模块加载规范
。代表库有SeaJS - CommonJS:
服务器端的模块加载规范
。代表库有Node.js - ES6 Module:
浏览器和服务器通用的模块加载规范
/** AMD写法 **/ define(["a", "b", "c", "d", "e", "f"], function(a, b, c, d, e, f) { // 等于在最前面声明并初始化了要用到的所有模块 a.doSomething(); if (false) { // 即便没用到某个模块 b,但 b 还是提前执行了 b.doSomething() } }); /** CMD写法 **/ define(function(require, exports, module) { var a = require('./a'); //在需要时申明 a.doSomething(); if (false) { var b = require('./b'); b.doSomething(); } }); /** CommonJs **/ module.exports = { doSomething: () => { var a = require('./a'); a.doSomething(); if (false) { var b = require('./b'); b.doSomething(); } } } /** ES6 Module **/ import { doSomething as aDoSomething } from './a'; import { doSomething as bDoSomething} from './b'; export { doSomething: () => { aDoSomething(); if (false) { bDoSomething(); } } };
- AMD
CommonJS 与 ES6 moudle
-
CommonJS
commonjs
是 Node 中的模块规范,通过require
及exports
进行导入导出 (进一步延伸的话,module.exports
属于commonjs2
)commonjs
模块可以运行在 node 环境及 webpack 环境下的,但不能在浏览器中直接使用。(前端使用时,会编译成浏览器可识别的语法)特点:
- 所有代码都运行在模块作用域,不会污染全局作用域。
- 模块加载是一项阻塞操作,也就是同步加载。即执行到require时才会加载模块,等待加载完成之后才会执行后面的代码。
- 同一模块多次加载,但是只会在第一次加载时运行一次,然后运行结果就被缓存了,以后再加载,就直接读取缓存结果。要想让模块再次运行,必须清除缓存。
- CommonJS模块的加载机制是,输入的是被输出的值的
浅拷贝
。即修改复杂类型数据对象时,会影响到另一个模块的值。
// lib.js var counter = 3; var obj = { counter: 3 }; function incCounter() { counter++; obj.counter++; } module.exports = { counter: counter, obj: obj, incCounter: incCounter, }; // main.js var counter = require('./lib').counter; var obj = require('./lib').obj; var incCounter = require('./lib').incCounter; console.log(counter); // 3 console.log(obj.counter); // 3 incCounter(); console.log(counter); // 3 console.log(obj.counter); // 4
-
ES6 Module
ES6 Module成为浏览器和服务器通用的模块解决方案
特点:
- import和export命令只能在模块的顶层,不能在代码块之中。
- ES6 可以在编译时就完成模块加载。ES6 的Module语法有些浏览器是不支持的,因此需要Babel先进性转码,将import和export命令转成ES5语法才能被浏览器解析。
-
CommonJS 与 ES6 moudle区别
- CommonJS模块是运行时加载(不需要编译),ES6 Modules是编译时输出接口
- CommonJS输出的是值的浅拷贝,基本数据类型的值输出不受被模块内部改变影响;ES6 Modules输出的是值的引用,模块的内部的改变会影响引用的改变
- CommonJS 模块的require()是同步加载模块,ES6 模块的import命令是异步加载,有一个独立的模块依赖的解析阶段。
- CommonJS this指向当前模块module.exports,ES6 Modules this指向undefined
import() 与 require()区别
import()
和 require()
是两种 JavaScript 模块加载的方式
-
模块规范
require()
是 Node.js 中的CommonJS模块加载方法,用于在服务器端环境中加载模块。它是同步加载的,通常用于静态模块的加载。import()
是 ECMAScript 模块规范(ES6/ES2015)中引入的动态导入语法,用于在浏览器和现代 JavaScript 环境中加载模块。它是异步加载的,通常用于动态模块的加载。
-
返回值
require()
返回模块的导出内容(exports),可以是对象、函数等。import()
返回一个 Promise,它在模块加载成功后会解析为导出的模块内容。
-
用途:
require()
通常用于 Node.js 服务器端开发,或者一些前端构建工具(如Webpack)的配置中。import()
用于在现代浏览器环境中动态加载模块,特别是在使用模块分割(code splitting)和懒加载(lazy loading)时非常有用。
const module = require('module-name');
import('module-name').then(module => {
// 模块加载成功后的操作
}).catch(error => {
// 模块加载失败后的操作
});
箭头函数与普通函数的区别
- 写法简洁
- 箭头函数内this指向外层作用域的this,所以在它在定义时已经确定了,之后不会改变
const func = () => { console.log(this) // window } func() // window
- call()、apply()、bind()等方法不能改变箭头函数中this的指向
var id = 1; const obj = {id:2} function func1(a,b) { console.log(this.id); } func1.call(obj,1,2); // 2 func1.apply(obj,[1,2]); // 2 func1.bind(obj,1,2)(); // 2 const func2 = (a,b) => { console.log(this.id); } func2.call(obj,1,2); // 1 func2.apply(obj,[1,2]); // 1 func2.bind(obj,1,2)(); // 1
- 箭头函数不能作为构造函数使用,所以没有prototype
const func = () => { } console.log(func.prototype) // undefined
- 箭头函数没有自己的arguments
function func1(a, b) { console.log(arguments[0]); // 1 console.log(arguments[1]); // 2 } func1(1, 2); const func2 = (a, b) => { console.log(arguments[0]); // error: arguments is not defined console.log(arguments[1]); // error: arguments is not defined } func2(1, 2);
- 箭头函数不能用作Generator函数,不能使用yeild关键字
对函数参数arguments与rest参数的理解
arguments与rest区别:
-
rest参数只包含那些没有对应形参的实参,而arguments对象包含了传给函数的所有实参
-
arguments对象不是一个真正的数组,而rest是真正的Array实例(真数组)
-
arguments对象还有一些附加属性(如callee属性)
// arguments变量的写法 function sortNumbers() { return Array.prototype.slice.call(arguments).sort(); } // rest参数的写法 const sortNumbers = (...numbers) => numbers.sort();
arguments对象:arguments对象是function(非箭头函数)中一个特殊的局部变量
-
arguments原型是Object,因此不能调用Array原型上的各种操作方法
-
arguments有内置的
Symbol(Symbol.iterator)
属性,可以被for..of遍历的// arguments转为Array [...arguments] // 或 Array.from(arguments) // 或 Array.prototype.slice.call(arguments); // 或 [].slice.call(arguments)
reset:ES6 引入 rest 参数(形式为...变量名
),这样就不需要使用arguments
对象了
- rest参数之后不能再有其他参数
- 函数的
length
属性,不包括 rest 参数。
(function(a) {}).length // 1
(function(...a) {}).length // 0
(function(a, ...b) {}).length // 1