JS 面试题整理
JS
this 相关
this 关键字是函数运行时自动生成的一个内部对象, 只能在函数内部使用, 总是指向调用他的对象
-
new 操作符具体干了什么
- 创建一个新的对象
- 将对象与构造函数通过原型链连接起来
- 将构造函数中的 this 指向新对象
- 返回这个新的对象
-
this 的绑定规则和各种情况下的指向
-
this 总是指向函数的直接调用者, 在函数执行的时候才能确定 this 的指向
-
this 的绑定规则: (优先级: new > 显式绑定 > 隐式绑定 > 默认绑定)
- 默认绑定 --> 在全局函数中定义函数内部使用 this, this 指向 window
- 隐式绑定 --> 将函数作为对象的某个方法调用时, this 指向这个方法的上一级对象
- 显式绑定 --> 通过 call, bind, apply 更改 this 指向
- new 绑定 --> 通过 new 生成一个实例对象, this 指向这个实例对象
-
this 在各种情况的指向
- 全局环境 --> 指向 window
- call, bind, apply 函数 --> 指向第一个参数
- 构造函数调用 --> 指向 new 出来的实例对象
- 箭头函数 --> 函数体内的 this 对象, 就是定义时所在的对象
-
-
箭头函数
箭头函数的 this 对象, 永远指向其上下文的 this, 任何方法都改变不了其指向, 普通函数的 this 指向调用他的那个对象
-
优点: 更简洁; 不绑定 this , 会捕获上下文的 this 值作为自己的 this 值;
-
特点:
- 箭头函数是匿名函数, 不能作为构造函数, 不可以使用 new 命令, 所以也没有原型属性 prototype;
- 箭头函数不能使用 yield 关键字, 所以箭头函数不能用作 Generator 函数;
-
意外
-
绑定事件监听(this 错误的指向了 window)
const button = document.getElementById("btn"); button.addEventListener("click", () => { console.log(this === window); this.innerHTML = "clicked button"; });
-
在原型上添加方法(this 错误的指向了 window)
-
-
-
call apply bind 的作用和区别
- 第一个参数都是 this 的指向
- apply 的第二个参数是一个数组, call 和 bind 的第二个参数及以后的参数需要一个一个传
- apply 和 call 绑定 this 后会立即调用, bind 不会, bind 会返回一个新函数
-
继承
原型和原型链
每个对象都会在其内部初始化一个属性, 就是 prototype(原型); 当我们访问一个对象的属性时, 如果这个对象内部不存在这个属性, 那么他就会去 prototype 里找这个属性, 这个 prototype 又会有自己的 prototype , 一直找下去, 一直检索到 Object 内建对象。这就是我们平时所说的原型链。
事件委托/事件代理
利用事件冒泡原理, 让自己所触发的事件由他的父级代替执行 优点:
- 减少事件注册, 提高性能
- 新增子元素不用重复绑定事件
缺点: Focus、blur 等事件没有冒泡机制, 所以无法事件委托
闭包以及应用场景
闭包: 一个函数内套一个函数并且返回了这个内部函数, 内部函数可以使用外部函数的参数
优点:
- 封装私有的方法和变量, 避免全局变量的污染;
- 延长变量生命周期;
应用:
- 回调函数
- 计时器
缺点: 闭包会使得函数中的变量都被保存在内存中, 内存消耗很大, 对性能有很大的负面影响, 而且参数和变量不会被垃圾回收机制回收, 可能导致内存泄露。 解决: 在退出函数之前, 将不使用的局部变量全部删除;
(function () {
var a = 1;
function add() {
const b = 2;
let sum = a + b;
console.log(sum); // 3
}
add();
})();
柯里化
把接受多个参数的函数转化成只接受一个单一参数的函数 优点:
- 让纯函数更纯, 每次接收一个函数, 松散解耦
- 惰性执行
// 非柯里化
var add = function (x, y) {
return x + y;
};
add(3, 4); // 7
// 柯里化
var add2 = function (x) {
return function (y) {
return x + y;
};
};
add(3)(4);
高阶函数
接受其他函数作为参数, 或返回其他函数的函数
function foo() {
var a = 2;
function bar() {
console.log(a);
}
return bar;
}
foo(); // 2
Promise
- 定义 --> 异步编程的一种解决方案, 解决了回调地狱的问题
- 状态
- pending --> 初始状态
- fulfilled --> 成功的操作
- rejected --> 失败的操作
- 特点:
- 外界不会影响到内部的状态;
- 状态一旦改变就不会再改变;
- 缺点:
- 如果没有回调函数, 外界得不到异步操作的返回结果
- 定义之后立即执行, 无法打断, 且无法得知进行进度
- Promise.all() --> 将多个 promise 实例包装成一个 promise, 多个实例全部返回成功, promise 的状态才为成功, 一个失败则为失败
- Promise.race() --> 意为赛跑的意思, 也就是数组中的任务哪个获取的块, 就返回哪个, 不管结果本身是成功还是失败, 一般用来和定时器绑定做超时提示
异步编程的实现方式
- 回调函数 --> 优点: 简单、容易理解; 缺点: 不利于维护, 代码耦合高
- 事件监听
- 观察者模式(发布/订阅)
- Promise 对象
- Generator 函数
- async 函数
ES6
- map sort reduce
- 模板字符串
- 展开运算符
- 解构赋值
- 箭头函数
- 提供了原生的 Promise 对象
- 增加了 let 和 const 命令, 用来声明变量
- 引入 module 模块的概念(export 和 import)
- for-of
ES7/8/9/10/11/12
-
ES7
- includes 方法
- 幂运算
**
- Math.pow(2, 3) // 8
- 2 ** 3 // 8
-
ES8
- async await -> promise 的语法糖
- Object.entries() -> 返回一个二维数组, 包含对象的键值对
- Object.keys() -> 返回一个数组, 包含对象的键
- Object.values() -> 返回一个数组, 包含对象的值
- Object.getOwnPropertyDescriptors() -> 返回一个数组, 包含对象的属性描述符
let obj = { a: 1, b: 2 }; Object.getOwnPropertyDescriptors(obj); // [a: {configurable: true, enumerable: true, value: 1, writable: true}, b: {configurable: true, enumerable: true, value: 2, writable: true}]
- padStart(targetLength, padString | 空格) -> 向字符串左侧填充指定的字符直到达到指定长度为止, 超出会被截断, 返回一个新的数组
- padEnd(targetLength, padString) -> 向字符串右侧填充指定的字符直到达到指定长度为止
-
ES9
- async iterators 异步迭代器, asyncIterator 对象的 next() 方法返回一个 promise
- Object rest properties -> 可以在对象中使用... 运算符, 展开剩余属性值
let test = { a: 1, b: 2, c: 3, d: 4, }; let { a, b, ...rest } = test; console.log(a); // 1 console.log(b); // 2 console.log(rest); // {c: 3, d: 4}
- Object spread properties
let test = { a: 1, b: 2, }; let result = { c: 3, ...test }; console.log(result); // {c: 3, a: 1, b: 2}
- Promise.prototype.finally -> 在 Promise 结束的时候, 不管是结果是 resolved 还是 rejected, 都会调用 finally 中的方法
const promise = new Promise((resolve, reject) => { resolve("resolved"); reject("rejectd"); }); promise .then((res) => { console.log(res); }) .finally(() => { console.log("finally"); });
-
ES10
- flat() 方法将数组扁平化为一维数组
- Object.fromEntries() -> 将键值对转换成对象
let map = new Map([ ["a", 1], ["b", 2], ]); let mapToObj = Object.fromEntries(map); console.log(mapToObj); // {a: 1, b: 2} let arr = [ ["a", 1], ["b", 2], ]; let arrToObj = Object.fromEntries(arr); console.log(arrToObj); // {a: 1, b: 2} let obj = { a: 1, b: 2 }; let newObj = Object.fromEntries( Object.entries(obj).map(([key, val]) => [key, val * 2]) ); console.log(newObj); // {a: 2, b: 4}
- trimStart/trimEnd -> 删除字符串头部或尾部的空格, 返回删除后的字符串 别名: trimLeft/trimRight
-
ES11
- 空值合并操作符(??) -> 如果左边的值为 null 或 undefined, 则返回右边的值 , 否则返回左边的值
undefined ?? "foo"; // 'foo' null ?? "foo"; // 'foo' "foo" ?? "bar"; // 'foo' // 逻辑或操作符(||) -> 会在左侧操作数为假值时返回右侧操作数, 在左侧操作数为 0、''、NaN、false 时返回右侧操作数 0 || "foo"; // 'foo' "" || "foo"; // 'foo' NaN || "foo"; // 'foo' false || "foo"; // 'foo'
- 可选链操作符(?) -> 允许读取位于连接对象链深处的属性的值, 而不必明确验证链中的每个引用都是否有效
- BigInt -> 类似于 JavaScript 中的 Number, 创建比 2^53 - 1(Number 可创建的最大数字) 更大的整数, 并且可以进行精确计算(解决 number 无法进行大数运算的问题)
- BigInt 不能用于 Math 对象中的方法;
- BigInt 不能与任何 Number 实例混合运算(但可以比较), 两者必须转换成同一种类型。但是需要注意, BigInt 在转换成 Number 时可能会丢失精度。
- 当使用 BigInt 时, 带小数的运算会被向下取整
- BigInt 和 Number 不是严格相等, 但是宽松相等
// 在一个整数字面量后面加 n, 例如 10n
-
ES12
- 逻辑运算符和赋值表达式(&&=, ||=, ??=)
// &&= -> x &&= y 等价于 x && (x=y) let a = 1; let b = 0; a &&= 2; console.log(a); // 2 b &&= 2; console.log(b); // 0 // ||= -> x ||= y 等价于 x || (x=y) // ??= -> x??= y 等价于 x?? (x=y)
- replaceAll(pattern, replacement) -> 字符串方法: 返回一个新字符串, 字符串中所有满足 pattern 的部分都会被 replacement 替换掉。原字符串保持不变。
// pattern 可以是一个字符串或 RegExp "aabbcc".replaceAll("b", "."); // 'aa..cc' // pattern 使用正则表达式搜索值时, 必须是全局的: "aabbcc".replaceAll(/b/, "."); // TypeError: replaceAll must be called with a global RegExp "aabbcc".replaceAll(/b/g, "."); // "aa..cc"
- 数字分隔符
- Promise.any
let,const 和 var 的区别
- 变量提升: var 可以, let const 也有变量提升, 但在声明之前不可使用, 不可使用的代码区称为暂时性死区(TDZ)
- 变量更改: var 和 let 可以, const 常量不可以 const 并不是让变量的值变得不可变, 而是让变量指向的内存地址不可变, 也就是说使用 const 声明的变量不能被重新赋值, 但是其所指向的内存中的数据是可以被修改的;
- 作用域: var 是函数作用域, let 和 const 是块级作用域
变量提升
在生成执行环境时, 会有两个阶段: 创建阶段和执行阶段 JS 解释器在创建阶段, 会找出需要提升的变量和函数, 并提前在内存中开辟好空间, 函数整个存⼊内存中, 变量提前声明并赋值为 undefined 到了代码执行阶段, 就可以直接使用
防抖和节流
一些浏览器事件如 resize scroll 等事件触发频率太高, 极大浪费资源、降低性能, 防抖和节流就是为了解决这个问题;
-
防抖(debounce) --> n 秒后再执行该事件, 若在 n 秒内重复触发, 则重新计时 应用: 搜索框搜索输入、手机号/邮箱验证 防抖:关注的是操作之间的间隔, 这个间隔时间大于等于 wait, 则执行一次。
-
节流(throttle) --> n 秒内只运行一次, 若在 n 秒内重复触发, 也只有一次生效 应用: 滚动加载/加载更多 节流:关注的是操作的过程, 给这个高频触发的过程添加限制, 让它固定 wait 时间只能执行一次。
函数式编程
主要的三种编程范式: 命令式编程、声明式编程、函数式编程 函数式编程更强调程序执行的结果而非过程, 利用简单的单元组合逐层实现复杂的运算, 而非设计一个复杂的执行过程 优点:
- 更好的管理状态(函数式编程宗旨无状态或更少的状态);
- 更简单的复用(纯函数的固定输入固定输出);
- 减少代码量, 提高可维护性
缺点:
- 资源占用
- 函数式编程中常用递归操作, 存在递归陷阱
-
纯函数: 纯函数= 无状态+数据不可变 概念:
-
只要是同样的输入, 必定得到同样的输出;
-
必须遵守以下约束:
- 不得改写参数数据;
- 不会产生任何副作用, 如网络请求;
- 不能调用 Date.now()或 Math.randow()等不纯的方法;
-
-
高阶函数
-
柯里化
断点续传
在上传或下载时, 将上传或下载任务人为的划分几个部分, 每一个部分采用一个线程进行上传或下载; 当遇到网络故障时, 可以从已下载或已上传的部分继续进行任务, 以节省时间, 提高速度
- 原理: 将上传文件在服务器写成临时文件, 全部完成后再重命名为正式文件即可。中断后可根据临时文件大小计算上传偏移量, 从此位置继续上传
- 实现方式: 当中断后重新上传时, 前端将唯一标识符发送给后端, 后端查找此任务是否已存在, 并返回相应的文件大小, 前端根据已上传进度继续上传
单点登录(sso/single sign on)
多个应用系统中, 用户只需要登录一次就可以访问所有相互信任的应用系统
- 前端在不同域名下实现单点登录的方式: 将 token 保存在 localstorage 中, 每次发送接口请求时携带此 token; 前端拿到 token 后, 不光可以存在本地的 localstorage 中, 还可以通过**iframe + postMessage()**写到多个其他域的 localstorage 中, 同样支持跨域
// 获取 token
var token = result.data.token;
// 动态创建一个不可见的 iframe, 在 iframe 中加载一个跨域HTML
var iframe = document.createElement("iframe");
iframe.src = "http://app1.com/localstorage.html";
document.body.append(iframe);
// 使用 postMessage() 方法将 token 传递给 iframe
setTimeout(function () {
iframe.contentWindow.postMessage(token, "http://app1.com");
}, 4000);
setTimeout(function () {
iframe.remove();
}, 6000);
// 在这个iframe所加载的HTML中绑定一个事件监听器, 当事件被触发时,
// 把接收到的 token 数据写入 localStorage
window.addEventListener(
"message",
function (event) {
localStorage.setItem("token", event.data);
},
false
);
递归
在函数定义中使用函数自身的函数
- 应用场景: 数组求和、数组扁平化、数组对象格式化
js 原生方法
-
字符串方法
-
trim
-
split --> 把字符串按照指定的分割符, 拆分成数组里的每一项
-
slice
-
substring
-
indexOf
-
substr
-
toUpperCase, toLoweCase
-
chatAt
-
includes
-
concat
"12+23+34".split("+"); --> [12,23,34]
-
-
数组方法
- concat --> 添加任意数量元素到数组末尾, 不改变原数组, 返回新数组
- push --> 添加元素到数组末尾
- shift 删除第一项
- splice --> 删除数组指定位置的元素, 传入开始位置和删除数量
- unshift --> 添加元素到数组开头
- pop 删除最后一项
// 第二个参数不传默认截取到末尾 [1, 2, 3].slice(1, 2) --> [1]
- slice --> 截取数组指定位置的元素, 传入开始位置和结束位置, 不改变原数组, 返回新数组
// 第二个参数不传默认截取到末尾 [1, 2, 3].slice(1, 2) --> [2, 3]
- indexOf
- includes
- find --> 返回第一个匹配的元素
- reverse --> 反转数组
- sort --> 排序
- join --> 传入分隔符, 转换数组
["a", "b", "c"].join(",") --> "a,b,c"
- some
- every
- filter --> 过滤
- map --> 循环-有返回值
- forEach --> 循环-无返回值
- flat --> 数组扁平化
- reduce --> 对数组中的每个元素执行一个自定义函数, 并将其结果汇总
数据类型及判断
-
js 数据类型
- 基础类型 number, string, boolean, undefined, null, symbol
- 复杂类型(引用类型) object, array, function
-
几种判断方式 基础类型用 typeof 判断, 引用数据类型用 constructor 和 Object.prototype.toString.call(target) 判断
-
typeof --> 对于基本类型, 除 null 以外, 均可以返回正确的结果(Null 返回 object); 对于引用类型, 除 function 以外, 一律返回 object 类型; 可以有效判断: number, string, boolean, undefined, symbol, object, function
// 正确判断: typeof 123; // number typeof "123"; // string typeof true; // boolean typeof undefined; // undefined typeof Symbol(); // symbol typeof {}; // object typeof func1(){}; // function // 错误判断: typeof null; // object
-
constructor(原型属性) --> 通过原型的 constructor 属性判断类型, null 和 undefined 没有 constructor,所以不能判断
const data = ""; data.constructor; // 可以判断 number, string, boolean, symbol, object, function, 不能判断 null, undefined
-
toString --> Object 的原型方法, 可以正确判断基本类型和复杂类型
const data = ""; Object.prototype.toString.call(data); // 可以判断 number, string, boolean, undefined, null, symbol, object, array, function
-
instanceof --> 是用来判断 A 是否为 B 的实例, 检测的是原型, 不能判断一个对象实例具体属于哪种类型; 由于数组也是对象, 因此 arr instanceof Object 也为 true; 可以准确判断复杂数据类型, 对于基础数据类型的判断不准确
-
通用写法:
const getType = (obj) => { let type = typeof obj; if (type !== "object") { return type; } return Object.prototype.toString .call(obj) .repalce(/^\[Object (\S+)\]$/, "$1"); };
-
-
如何判断对象 --> Object.prototype.toString.call(obj)
-
如何判断数组 --> constructor, Object.prototype.toString.call(arr), isArray
-
如何判断空 --> !value && typeOf(value)!== undefined && value !== 0
-
如何判断对象相等 --> 用 JSON.stringify() 转换为字符串后再判断
-
类型转换机制
- 显式转换 --> Number(), toString(), parseInt(), Boolean();
- 隐式转换 --> 比较运算符: == != > < if while 算数运算符: + - * / %
堆栈
- 栈: 基础数据类型 --> 占据空间小 、大小固定, 属于被频繁使用数据, 所以放入栈中存储 自动分配相对固定大小的内存空间, 并由系统自动释放, 内存可以及时回收
- 堆: 复杂数据类型(引用数据类型) --> 占据空间大 、大小不固定, 如果存储在栈中, 将会影响程序运行的性能; 引用数据类型在栈中存储了指针, 该指针指向堆中的实体 动态分配内存, 内存大小不一, 也不会自动释放;
-
深拷贝和浅拷贝
-
浅拷贝: 基本类型拷贝数据, 引用类型拷贝引用地址/指针 方法: Object.assign(), Array.prototype.slice(), Array.prototype.concat(), 扩展运算符实现的复制
-
深拷贝: 开辟一个新的栈, 两个属性完全相同但对应两个不同的引用地址 方法: _.cloneDeep(), jQuery.extend(), JSON.stringify() JSON.stringify() 弊端:会忽略 undefined, symbol 和函数
-
-
手写深拷贝
数组和对象的遍历方式
- for in: 需要分析出 array 的每个属性, 这个操作性能开销很大; 用在 key 已知的数组上是非常不划算的。所以尽量不要用 for-in , 除非你不清楚要处理哪些属性, 例如 JSON 对象这样的情况;
- for: 循环每进行一次, 就要检查一下数组长度。读取属性(数组长度)要比读局部变量慢, 尤其是当 array 里存放的都是 DOM 元素, 因为每次读取都会扫描一遍页面上的选择器相关元素, 速度会大大降低;
- forEach: 无法遍历对象, 无返回值;
常用方法
-
去重
-
ES6 set 去重:
function unique(arr) { return Array.from(new Set(arr)); } var arr = [ 1, 1, "true", "true", true, true, 15, 15, false, false, undefined, undefined, {}, {}, ]; console.log(unique(arr)); // [1, "true", true, 15, false, undefined, null, NaN, "NaN", 0, "a", {}, {} // 空对象 {} 不重复 // 优化: [...new Set(arr)];
-
filter 去重
function unique(arr) { return arr.filter(function (item, index, arr) { //当前元素, 在原始数组中的第一个索引==当前索引值, 否则返回当前元素 return arr.indexOf(item, 0) === index; }); }
-
reduce+includes 去重
function unique(arr){ return arr.reduce((prev,cur) => prev.includes(cur) ? prev : [...prev,cur] }
-
其他: 递归, hasOwnProperty, for 循环 + indexOf + push 新数组, for 循环嵌套 + splice...
-
-
排序
- 冒泡排序
- 比较相邻的元素。如果第一个比第二个大, 就交换它们两个;
- 对每一对相邻元素作同样的工作, 从开始第一对到结尾的最后一对, 这样最终最大数被交换到最后的位置;
- 除了最后一个元素以外, 针对所有的元素重复以上的步骤;
- 重复步骤 1~3, 直到排序完成.
- 快速排序
- 选取基准元素;
- 比基准元素小的元素放到左边, 大的放右边;
- 在左右子数组中重复步骤一二, 直到数组只剩下一个元素;
- 向上逐级合并数组.
- 插入排序
- 从第一个元素开始, 该元素默认已经被排序;
- 取出下一个元素, 在已经排序的元素序列中从后向前扫描;
- 如果该元素(已排序)大于新元素, 将该元素移到下一位置;
- 重复步骤 3, 直到找到已排序的元素小于或者等于新元素的位置
- 将新元素插入到该位置;
- 重复步骤 2~5, 直到排序完成.
- sort 原理: 快排和插入排序算法
Map(字典) 和 Set(集合) 的区别
Map 和 Set 都是构造函数, 以 new Set()或者 new Map()的形式初始化; Map 生成的是类对象(Map 对象)的数据结构, Set 生成的是类数组(Set 对象)的数据结构;
-
Map:
-
特点:
- 以键值对的形式(key-value)存储数据;
- Map 对象存储的数据是有序的, Object 对象是无序的;
- Map 对象的键值可以是任意类型, 而 Object 键只能是字符串;
-
使用:
// 初始化 let myMap = new Map(); // 接收初始值的初始化: 默认接收一个二维数组 let defaultMap = new Map([ ["name", "张三"], ["age", 20], ]); // defaultMap 展开: [ { key: "name", value: "张三" }, { key: "age", value: 20 }, ]; // 插入数据(键可以是任意类型): defaultMap.set(102, "number"); // number 类型作为键 defaultMap.set({}, "某对象"); // 对象类型作为键 展开: [ { key: "name", value: "张三" }, { key: "age", value: 20 }, { key: 102, value: "number" }, { key: {}, value: "某对象" }, ]; // 获取长度 defaultMap.size; // 4 // 获取对应键值 defaultMap.get(102); // "number" // 删除对应键值对 defaultMap.delete(102); // 查找某个值是否存在 defaultMap.has("name");
-
-
Set:
-
特点:
- Set 对象存储的值是不重复的, 可以利用这一点实现数组去重;
let arr = [1, 2, 3, 4, 5, 6, 3, 2, 5, 3, 2]; console.log([...new Set(arr)]); // [1, 2, 3, 4, 5, 6] // 扩展运算符的作用是将 Set 类数组转换成数组;
- Set 对象可以存储任意类型的值, 不以键值对形式存储;
- Set 对象存储的值是不重复的, 可以利用这一点实现数组去重;
-
使用:
// 初始化: let mySet = new Set(); // 接收数组作为初始值: let defaultSet = new Set(["张三", 18, true]); // defaultSet 展开: { "张三", 12, true; } // 插入数据: defaultSet.add("李四"); // 获取长度: defaultSet.size; // 4 // 获取值: defaultSet.forEach((item) => { console.log(item); }); // 删除值: defaultSet.delete("李四"); // 查找某个值是否存在: defaultSet.has(18);
-
- 区别:
- Map 初始值是一个二维数组, 而 Set 是一维数组;
- Map 的键是不能修改, 但是键值是可以修改的;Set 只能通过迭代器来改变 Set 的值, 因为 Set 的值就是键;
- has 查找速度很快, 时间复杂度 O(1), 而数组查找的时间复杂度是 O(n);
- Map 对象和 Set 对象有唯一性都不允许键重复;
设计模式
-
单例模式:
const Singleton = (function () { let instance; function createInstance() { // 创建单例对象的代码 return { // 单例对象的属性和方法 }; } return { getInstance: function () { if (!instance) { instance = createInstance(); } return instance; } }; })(); // 使用单例对象 const singletonInstance1 = Singleton.getInstance(); const singletonInstance2 = Singleton.getInstance(); console.log(singletonInstance1 === singletonInstance2); // true
-
工厂方法模式:
-
建造者模式:
-
原型模式:
-
代理模式:
-
适配器模式:
-
桥接模式:
-
组合模式:
-
装饰器模式:
-
享元模式:
-
策略模式:
-
模板方法模式:
-
观察者模式:
-
状态模式:
-
访问者模式: