前言
借鉴了网上一些资料,整理了一些 JavasSript
相关的高频面试题,方便自己随时查阅复习、查缺补漏,温故而知新!也希望帮助到其他同学。如有错误,欢迎评论区指正!
js重点
作用域&&作用域链
-
作用域: 规定了如何查找变量,也就是确定当前执行代码对变量的访问权限。换句话说,作用域决定了代码区块中变量和其他资源的可见性(全局作用域、函数作用域、块级作用域)
-
作用域链: 从当前作用域开始一层层往上找某个变量,如果找到全局作用域还没找到,就放弃寻找。这种层级关系就是作用域链。(由多个执行上下文的变量对象构成的链表就叫做作用域链)
js原型&&原型链
原型:
当试图访问一个对象的属性时,它不仅在该对象上搜寻,还会搜寻该对象的原型,以及该对象的原型的原型,依次层层向上搜索,直到找到一个名字匹配的属性或到达原型链的末尾。
准确地说,这些属性和方法 定义在
Object
的构造器函数(constructor functions)之上的prototype
属性上,而非实例对象本身。
例子:
js
function doSomething(){}
console.log(doSomething.prototype);
输出原型对象,原型对象有一个自有属性constructor
,这个属性指向该函数:
js
{
constructor: ƒ doSomething(),
__proto__: {
constructor: ƒ Object(),
hasOwnProperty: ƒ hasOwnProperty(),
isPrototypeOf: ƒ isPrototypeOf(),
propertyIsEnumerable: ƒ propertyIsEnumerable(),
toLocaleString: ƒ toLocaleString(),
toString: ƒ toString(),
valueOf: ƒ valueOf()
}
}
constructor
又指向 doSomething
该函数
原型链:
-
原型可以解决什么问题?
对象共享属性、共享方法
-
谁有原型?
函数拥有:
prototype
、对象拥有:__proto__
-
对象查找属性或者方法的顺序?
先在对象本身查找 --> 构造函数中查找 --> 对象的原型 --> 构造函数的原型中 --> 当前原型的原型中查找; 这个查找的过程称之为:原型链,原型链的最顶端是null。
-
__proto__
原型指针
__proto__
作为不同对象之间的桥梁,用来指向创建它的构造函数的原型对象的每个对象的
__proto__
都是指向它的构造函数的原型对象prototype
总结:
- 一切对象都是继承自
Object
对象,Object
对象直接继承根源对象null
- 一切的函数对象(包括
Object
对象),都是继承自Function
对象 Object
对象直接继承自Function
对象Function
对象的__proto__
会指向自己的原型对象,最终还是继承自Object
对象
new操作符做了什么
new操作符共经历了四个过程:
- 1、首先创一个新的空对象
- 2、设置空对象的
__proto__
为构造函数的prototype
- 3、让fn的this指向新对象obj,并执行fn的函数体 (构造函数fn的this 指向这个对象,执行构造函数的代码(为这个新对象添加属性))
- 4、判断fn的返回值类型,如果是引用类型,就返回这个引用类型的对象。如果是值类型,返回obj
手写一个new操作符:
js
function myNew (fn,...args) {
// 1、创建一个空对象
// const obj = new Object()
// 2、新对象obj 的_proto_指针指向构造函数fn的prototype属性
// obj.__proto__ = fn.prototype
// 1、2步合并写法
const obj = Object.create(fn.prototype)
// 3、将构造函数fn 内部的this指向新对象(obj), this指向新对象,并执行构造函数代码
const res = fn.apply(obj,args) // apply 接受的参数是数组
// 4、判断结果,如果构造函数返回的是对象,则使用构造函数执行的结果res。否则,返回新创建的新对象obj
return typeof res === Object ? res : obj
}
使用:
js
// 创建一个构造函数
function People(name,age) {
this.name = name; // 实例上的属性
this.age = age; // 实例上的属性
}
const ada = myNew(People, '张三',18)
console.log('-----:',ada.name) // 张三
console.log('-----:',ada.age) // 18
js中this指向
this是什么?
this是一个对象,但是我们不同的操作 this指向的对象是不相同的
5种this指向情况:
- 当函数作为对象的方法被调用时,
this
就会指向该对象
。 - 作为普通函数执行时,
this
指向全局变量window
。 - 构造器调用(使用new),
this
指向返回的这个对象
。 - 箭头函数的
this
绑定的是this所在函数定义在哪个对象下
,就绑定哪个对象。如果有嵌套的情况,则this绑定到最近的一层对象上。 - 基于
Function.prototype
上的apply 、 call 和 bind
调用模式,这三个方法都可以显式的指定调用函数的 this 指向:
apply
接收参数的是数组,
call
接受参数是列表,
bind
方法通过传入一个对象,返回一个this
绑定了传入对象的新函数。这个函数的this
指向除了使用new
时会被改变,其他情况下都不会改变。若为空默认是指向全局对象window
js闭包
概念: 一个定义在函数内部的函数,可以读取到其他函数内部变量的函数,本质上,闭包就是一个把函数内部和外部连接起来的桥梁。
一句话总结: 闭包就是能够读取其他函数内部变量的函数
闭包原理: 利用了作用域链的特性,作用域链就是在当前执行环境下访问某个变量时,如果不存在就一直向外层寻找,最终寻找到最外层也就是全局作用域,这样就形成了一个链条。
闭包用途:封装私有化变量
模仿块级作用域
保护外部函数的变量 能够访问函数定义时所在的词法作用域(阻止其被回收)
创建模块
缺点: 变量会驻留在内存中,造成内存损耗,不恰当的使用闭包可能会造成内存泄漏的问题
闭包应用案例分析
需求:实现变量a 自增
- 1、通过全局变量可以实现,但会污染其他程序
js
var a = 10;
function Add(){
a++;
console.log(a);
}
Add();
Add();
Add();
- 2、定义一个局部变量,不污染全局,但是实现不了递增
js
var a = 10;
function Add2(){
var a = 10;
a++;
console.log(a);
}
Add2();
Add2();
Add2();
console.log(a);
- 3、通过闭包实现,可以使函数内部局部变量递增,不会影响全部变量,完美!!
js
var a = 10;
function Add3(){
var a = 10;
return function(){
a++;
return a;
};
};
var cc = Add3();
console.log(cc());
console.log(cc());
console.log(cc());
console.log(a);
闭包其他应用:
-
从外部读取函数内部的变量
jsfunction f1(){ var n=111; function f2(){ console.log(n); } return f2; } var result = f1(); result(); // 111 // 把f2作为返回值,就可以在f1外部读取它的内部变量
-
将创建的变量的值始终保持在内存中
jsfunction f1() { var n = 12; function f2() { console.log(++n); }; return f2; } var result = f1(); result();//13 //函数f1中的局部变量n一直保存在内存中,并没有在f1调用后被销毁
-
封装对象的私有属性和私有方法
jsfunction f1(n) { return function () { return n++; }; } var a1 = f1(1); a1() // 1 a1() // 2 a1() // 3 var a2 = f1(5); a2() // 5 a2() // 6 a2() // 7 //这段代码中,a1 和 a2 是相互独立的,各自返回自己的私有变量。
防抖、节流
防抖: n 秒后在执行该事件,若在 n 秒内被重复触发,则重新计时
防抖场景:
- 搜索框搜索输入;只需用户最后一次输入完,再发送请求
- 手机号、邮箱验证输入检测
- 窗口大小
resize
;只需窗口调整完成后,计算窗口大小,防止重复渲染
节流: n 秒内只运行一次,若在 n 秒内重复触发,只有一次生效
节流场景:
- 滚动加载更多
- 搜索框搜索联想功能
- 高频点击
- 表单重复提交
浅拷贝、深拷贝
-
浅拷贝:
只复制了引用,而不复制真正的值,新旧对象还是共享同一块内存(分支)
方法:
- 扩展运算符: ...
- Object.assign()
- slice
- concat
缺点: 浅拷贝只能拷贝第一层的数据,且都是值类型数据,当对象内嵌套有对象的时候,被嵌套的对象进行的还是浅拷贝;
-
深拷贝:
会另外创造一个一模一样的对象,新对象跟原对象不共享内存,修改新对象不会影响原对象,是"值"而不是"引用"(不是分支)
方法:
-
JSON.parse(JSON.stringify())
当要拷贝的数据中含有undefined/function/symbol类型会忽略;
不可以对
Function
进行拷贝,因为JSON
格式字符串不支持Function
,在序列化的时候会自动删除;Map
,Set
,RegExp
,Date
,ArrayBuffer
和其他内置类型在进行序列化时会丢失; -
jQuery
的extend
-
lodash的
_.cloneDeep()
-
手写一个递归实现深拷贝
例:(有循环引用的问题)
jsfunction cloneDeep(obj){ const newObj = {}; let keys = Object.keys(obj); let key = null; let data = null; for(let i = 0; i<keys.length;i++){ key = keys[i]; data = obj[key]; if(data && typeof data === 'object'){ newObj[key] = cloneDeep(data) }else{ newObj[key] = data; } } return newObj }
-
js的垃圾回收机制
Javascript 具有自动垃圾回收机制,会定期对那些不再使用的变量、对象所占用的内存进行释放,原理就是找到不再使用的变量,然后释放掉其占用的内存。
垃圾回收策略
- 标记清除( Mark-Sweep )
目前在 JavaScript引擎 里这种算法是最常用的,到目前为止的大多数浏览器的 JavaScript引擎 都在采用标记清除算法,只是各大浏览器厂商还对此算法进行了优化加工,且不同浏览器的 JavaScript引擎 在运行垃圾回收的频率上有所差异。
此算法分为 标记 和 清除 两个阶段,标记阶段:即为所有活动对象做上标记。清除阶段:则把没有标记(也就是非活动对象)销毁。
首先它会遍历堆内存上所有的对象,分别给它们打上标记,然后在代码执行过程结束之后,对所使用过的变量取消标记。在清除阶段再把具有标记的内存对象进行整体清除,从而释放内存空间。
优点:简单
缺点:内存碎片化、分配速度慢
- 引用计数( Reference Counting )
这其实是早先的一种垃圾回收算法,它把对象是否不再需要简化定义为对象有没有其他对象引用到它,如果没有引用指向该对象(零引用),对象将被垃圾回收机制回收,但因为它的问题很多,目前很少使用这种算法了。
详情参考: Javascript的垃圾回收机制知多少? - 掘金 (juejin.cn)
js内存泄露
内存泄漏,指在JS中已经分配内存地址的对象由于长时间未进行内存释放或无法清除 ,造成了长期占用内存,使得内存资源浪费, 最终导致运行的应用响应速度变慢以及最终崩溃的情况。
造成内存泄露的原因:
- 滥用闭包
- 意外的全局变量
- 被遗忘的定时器、回调函数
- 脱离DOM的引用
- 注意程序逻辑,避免编写『死循环』之类的代码
- DOM对象和JS对象相互引用
- 反复重写同一个数据会造成内存大量占用,但是IE浏览器关闭后会被释放。
js变量提升
变量提升(hoisting)是用于解释代码中变量声明行为的术语。
使用var
关键字声明或初始化的变量,会将声明语句"提升"到当前作用域的顶部 (也就是说在变量声明前访问它是可以访问到该变量的,只不过它的值是undefined
)但是,只有声明才会触发提升,而赋值语句(如果有的话)将保持原样。
- var、let声明变量
js
// 用 var 声明会得到提升,也就是在声明前访问也是可以的
console.log(foo); // undefined
var foo = 1;
console.log(foo); // 1
// 用 let/const 声明不会提升,声明前访问直接报错
console.log(bar); // ReferenceError: bar is not defined
let bar = 2;
console.log(bar); // 2
- 函数声明
函数声明会使函数体提升 ,也就是声明之前可以访问到;
但函数表达式 (以声明变量的形式书写)只有变量声明会被提升。
js
// 函数声明,声明前访问可以访问到函数体
console.log(foo); // [Function: foo]
foo(); // 'FOOOOO'
function foo() {
console.log('FOOOOO');
}
console.log(foo); // [Function: foo]
// 函数表达式声明时,bar该变量声明会被提升,此时的函数体不会提升也就报错了
console.log(bar); // undefined
bar(); // Uncaught TypeError: bar is not a function
var bar = function () {
console.log('BARRRR');
};
console.log(bar); // [Function: bar]
js执行上下文
执行上下文(Execution Context)是JavaScript中的一个核心概念。它代表了代码被执行时的环境和条件。换句话说,执行上下文是一个抽象的概念,涵盖了变量、函数和它们的作用域。
每当我们运行JavaScript代码时,都会创建一个执行上下文。这个上下文可以是全局的,也可以是与特定函数相关的。理解执行上下文对于我们正确理解代码的运行机制至关重要。
执行上下文的种类
在JavaScript中,存在不同类型的执行上下文,每种都在特定的情况下创建。
全局执行上下文
全局执行上下文是代码中最外层的上下文,它在整个脚本执行期间都存在。在浏览器环境中,全局执行上下文通常与window
对象相关联。
函数执行上下文
每次调用函数时,都会创建一个新的函数执行上下文。函数执行上下文与全局执行上下文类似,但它仅在函数调用期间存在。
块级执行上下文
ES6引入了
let
和const
关键字,它们允许我们创建块级作用域。块级执行上下文与函数执行上下文类似,但它们存在于用花括号{}
定义的块中。
描述事件冒泡
当一个事件在 DOM 元素上触发时,如果有事件监听器,它将尝试处理该事件,然后事件冒泡到其父级元素,并发生同样的事情。
最后直到事件到达祖先元素。事件冒泡是实现事件委托的原理。
微任务、宏任务(Event Loop)
概念: js 是一种单线程语言 。
简单来说:只有一条通道,那么在任务多的情况下,就会出现拥挤的情况,这种情况下就产生了 多线程 ,但是这种多线程 是通过单线程模仿的,也就是假的。那么就产生了同步任务 和异步任务 微任务:DOM 渲染前 触发;宏任务:DOM 渲染后触发;
宏任务(macrotask) | 微任务(microtask) | |
---|---|---|
谁发起的 | 宿主(Node、浏览器) | JS引擎 |
具体事件 | 1.script(可以理解为外层同步代码); 2.setTimeout/setInterval; 3.UI rendering/UI事件; 4.DOM 事件; 5.setImmediate(Node.js) | 1.Promise; 2.MutaionObserver; 3.Object.observe(已废弃;Proxy 对象替代);4.process.nextTick(Node.js) |
谁先运行 | 后运行 | 先运行 |
会触发新一轮Tick吗 | 会 | 不会 |
Event Loop运行机制
在事件循环中,每进行一次循环操作称为 tick,每一次 tick 的任务处理模型是比较复杂的,但关键步骤如下:
- 执行一个宏任务(栈中没有就从事件队列中获取)
- 执行过程中如果遇到微任务,就将它添加到微任务的任务队列中
- 宏任务执行完毕后,立即执行当前微任务队列中的所有微任务(依次执行)
- 当前宏任务执行完毕,开始检查渲染,然后GUI线程接管渲染
- 渲染完毕后,JS线程继续接管,开始下一个宏任务(从事件队列中获取)
例一
js
setTimeout(function(){
console.log('1');
});
new Promise(function(resolve){
console.log('2');
resolve();
}).then(function(){
console.log('3');
}).then(function(){
console.log('4')
});
console.log('5');
//输出为: 2 5 3 4 1
分析:
1.遇到setTimout,异步宏任务,放入宏任务队列中
2.遇到new Promise,new Promise在实例化的过程中所执行的代码都是同步进行的,所以输出2
3.而Promise.then中注册的回调才是异步执行的,将其放入微任务队列中
4.遇到同步任务console.log('5');输出5;主线程中同步任务执行完
5.从微任务队列中取出任务到主线程中,输出3、 4,微任务队列为空
6.从宏任务队列中取出任务到主线程中,输出1,宏任务队列为空
浏览器输入url回车后发生了什么
- 读取缓存:搜索自身的 DNS 缓存(如果 DNS 缓存中找到IP 地址就跳过了接下来查找 IP 地址步骤,直接访问该 IP 地址)
- DNS 解析: 将域名解析成 IP 地址
- TCP 连接:TCP 三次握手,简易描述三次握手:
- 客户端:服务端你在么?
- 服务端:客户端我在,你要连接我么?
- 客户端:是的,我要链接。连接打通,可以开始请求了
- 发送 HTTP 请求
- 服务器处理请求并返回 HTTP 报文
- 浏览器解析渲染页面
- 根据 HTML 解析出 DOM 树
- 根据 CSS 解析生成 CSS 规则树
- 结合 DOM 树和 CSS 规则树,生成渲染树
- 根据渲染树计算每一个节点的信息
- 根据计算好的信息绘制页面
- 断开连接:TCP 四次挥手
什么是高阶函数
-
可以把函数当作参数传递给另外一个函数
-
可以把函数当作另一个函数的返回结果
使用高阶函数的意义:
抽象可以帮我们屏蔽细节,只需要关注于我们的目标,高阶函数用来抽象通用的问题
常用高阶函数: forEach、map、filter、sort、some、reduce等
高频八股文
js 数据类型
基本类型
js有 8
种基础的数据类型,分别为: Undefined
、 Null
、 Boolean
、 Number
、 String
、 Object
、 Symbol
、 BigInt
Symbol
和 BigInt
是 ES6 新增的数据类型
- Symbol 代表独一无二的值,最大的用法是用来定义对象的唯一属性名。
- BigInt 可以表示任意大小的整数。
引用类型 Object
js内置引用类型:Array
、Function
、Date
, RegExp
等
基本类型和引用类型主要区别
存放位置:
- 基本数据类型:基本类型值在内存中占据固定大小,直接存储在栈内存中的数据
- 引用数据类型:引用类型在栈中存储了指针,这个指针指向堆内存中的地址,真实的数据存放在堆内存里
比较:
- 基本数据类型: 基本类型的比较是值的比较,只要它们的值相等就认为他们是相等的
- 引用数据类型: 引用数据类型的比较是引用的比较,看其的引用是否指向同一个对象
typeof 与 instanceof的区别?
- typeof
typeof是一个操作符,其右侧跟一个一元表达式,并返回这个表达式的数据类型。返回的结果用该类型的字符串(全小写字母)形式表示,包括以下 7 种:number、boolean、symbol、string、object、undefined、function 等。
能判断所有值类型,函数。不可对 null、对象、数组进行精确判断,因为都返回 object
- instanceof
instanceof是用来判断 A 是否为 B 的实例,表达式为:
A instanceof B
如果A是B的实例,则返回 true ,否则返回 false 。 在这里需要特别注意的是:instanceof 检测的是原型。
instanceof 判断两个对象是否属于实例关系,而不能判断一个对象实例具体属于哪种类型。能判断对象类型,不能判断基本数据类型,其内部运行机制是判断在其原型链中能否找到该类型的原型。
instanceof原理
instanceof 用于检测一个对象是否为某个构造函数的实例。
例如:A instanceof B
instanceof 用于检测对象 A 是不是 B 的实例,而检测是基于原型链进行查找的,也就是说 B 的 prototype 有没有在对象 A 的__proto __ 原型链上,如果有就返回 true ,否则返回 false
为什么ES6会新增Promise
在 ES6 以前,解决异步的方法是回调函数。但是回调函数有一个最大的问题就是回调地狱(callback hell),当我们的回调函数嵌套的层数过多时,就会导致代码横向发展。
Promise 的出现就是为了解决回调地狱的问题。
同步和异步的JavaScript代码执行方式
同步代码是按照顺序执行的代码,每个任务必须等待前一个任务完成后才能执行。同步代码会阻塞后续代码的执行,直到当前任务完成。
异步代码是不按照顺序执行的代码,它会在后台执行,不会阻塞后续代码的执行。异步代码通常使用回调函数、Promise、async/await等方式来处理异步操作的结果。
通过异步执行,可以避免阻塞主线程,提高页面的响应性能。
Es6新特性
ES6的严格模式
- 变量必须声明后在使用
- 函数的参数不能有同名属性, 否则报错
- 不能对只读属性赋值, 否则报错
- 不能删除不可删除的数据, 否则报错
- 禁止this指向全局对象
- 增加了保留字(比如protected、static和interface)
let和const新增的变量声明
箭头函数、变量的解构赋值、反引号模板字符串
新增Symbol数据类型
Set 和 Map 数据结构
- ES6 提供了新的数据结构 Set。它类似于数组,但是成员的值都是唯一的,没有重复的值。 Set 本身是一个构造函数,用来生成 Set 数据结构。
- Map它类似于对象,也是键值对的集合,但是"键"的范围不限于字符串,各种类型的值(包括对象)都可以当作键
Proxy、Reflect
- Proxy 可以理解成,在目标对象之前架设一层"拦截",外界对该对象的访问都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写
- Reflect 反射对象,可以实现对 对象的增删改查
Promise
Promise 是异步编程的一种解决方案,比传统的解决方案------回调函数和事件------更合理和更强大
特点是:对象的状态不受外界影响。一旦状态改变,就不会再变,任何时候都可以得到这个结果
async 函数
async函数的返回值是 Promise
对象,这比 Generator
函数的返回值是 Iterator
对象方便多了; 可以用 then
方法指定下一步的操作
class
- class跟let、const一样:不存在变量提升、不能重复声明等
- ES6 的class可以看作只是一个语法糖,它的绝大部分功能ES5 都可以做到,新的class写法只是让对象原型的写法更加清晰、更像面向对象编程的语法而已。
Module
- ES6 的模块自动采用严格模式,不管你有没有在模块头部加上
"use strict";
import
和export
命令以及export
和export default
的区别
字符串的扩展
- includes():返回布尔值,表示是否找到了参数字符串
- startsWith():返回布尔值,表示参数字符串是否在原字符串的头部
- endsWith():返回布尔值,表示参数字符串是否在原字符串的尾部
数值的扩展
- Number.isFinite()用来检查一个数值是否为有限的(finite)。
- Number.isNaN()用来检查一个值是否为NaN。