JavaScript 由那三部分组成?
- ECMASript
- 文档对象模型(DOM)
- 浏览器对象模型(BOM)
ES6 新特性
let
const
... 扩展运算符
set
map
promise
generator
proxy
module --> import export
什么是 DOM 和 BOM ?
DOM 指的是文档对象模型,它指的是把文档当做一个对象来对待,这个对象主要定义了处理网页内容的方法和接口。
BOM 指的是浏览器对象模型,它指的是把浏览器当做一个对象来对待,这个对象主要定义了与浏览器进行交互的方法和接口。BOM的核心是 window,而 window 对象具有双重角色,它既是通过 js 访问浏览器窗口的一个接口,又是一个 Global(全局)对象。这意味着在网页中定义的任何对象,变量和函数,都作为全局对象的一个属性或者方法存在。window 对象含有 location 对象、navigator 对象、screen 对象等子对象,并且 DOM 的最根本的对象 document 对象也是 BOM 的 window 对象的子对象。
说一说 JS 数据类型有哪些,区别是什么?
js 主要有两种数据类型:基本数据类型 和 复杂数据类型。
基本数据类型存储在栈中,存储的是基本数据的原始值,所以也叫做原始数据类型。
复杂数据类型存储在堆中,存储的是复杂数据类型的地址引用,所有也叫做引用数据类型。
基本数据类型:
在 ES6 之前,基本数据类型有五种:string、number、boolean、undefined、null。
以及 ES6 新增的 Symbol,ES10 新增的 BigInt。
Symbol 它表示创建一个独一无二的值,我认为它的出现主要是为了解决可能存在的变量命名重复问题。
BigInt 它可以表示任意精度的整数,使用 BigInt 可以安全的存储和操作大整数,常用在超出 Number 能够表示的安全整数的情况。
Symbol 和 BigInt 都不是构造函数,所以不能被 new。
引用数据类型:
主要是指 Object 类型,Array、Math、Date 等都属于 Object 的子类。
栈和堆的区别:
栈是一种先入后出的数据结构,而堆没有什么规律,它只用一块足够大的空间来存储变量。
栈区内存由编译器自动释放,程序员不需要关心,存放函数的参数值,局部变量的值等。
堆区内存一般由程序员手动释放,如果程序员不释放,程序结束时可能由垃圾回收机制回收。
null 和 undefined?
首先 Null 和 Undefined 都是基本数据类型,这两个基本数据类型分别都只有一个值,就是 null 和 undefined。
undefined 代表的含义是未定义,null 代表的含义是空对象。一般变量声明了但还没有定义(赋值)的时候回返回 undefined,null 主要用于对对象附初始值。标识这个变量之后要作为一个对象去使用。直接将变量设置为 null 也常用于释放对象所占据的内存。
当我们对两种数据类型进行 typeof 判断的时候,Undefined 类型 会返回 ”undefined“,Null 类型会返回 “object”,这是一个历史遗留问题。当我们使用“==”对两种类型的数据进行比较时会返回 true,使用“===”时会返回 false。
如何获取安全的 undefined 值?
因为 undefined 是一个标识符(不是保留字),所以可以被当做变量来使用和赋值,也就是这种写法:let undefined = 10; 这样会影响 undefined 的正常判断。
表达式 void __ 没有返回值,返回结果是 undefined。void 并不改变表达式的结果,只是让表达式不返回值。
按照惯例我们可以用 void 0 来获取 undefined。
以下情况会返回 undefined:
- 当一个变量没有被赋值。
- 一个函数没有返回值。
- 访问某个对象内没有的属性。
- 函数定义了形参但是没有传递实参。
0.1 + 0.2 !== 0.3
计算机中是使用过二进制进行计算的,首先将十进制转化为二进制。
在二进制中,十进制浮点数无法完全精确转换为二进制浮点数。
1 + 2 = 3
0000 0001 +
0000 0010 =
0000 0011 --> 3
说几种 JS 判断数据类型的方法
1. typeof 只能判断基本数据类型,typeof null === 'object', 其他基本数据类型都正常。对于引用数据类型,都是 'object'。
2. instanceof 只能判断引用数据类型,左边实例对象,右边构造函数,基本原理是判断实例对象的 __proto__ 属性是否指向构造函数的 prototype,如果不相等,则查询实例对象的 __proto__.__proto__ 依次在原型链上查找,直到找到构造函数的 prototype,如果查找失败,则会返回 false。
3. Object.prototype.toString.call() 推荐使用的方式,返回格式如下:“[object Undefined]“。
js 有那些内置对象?
浏览器对象:
- window
- document
- location
- history
- navigator 对象包含有关浏览器的信息,通常用于检测浏览器与操作系统的版本。
- screen 对象用于获取用户的屏幕信息。
全局JavaScript对象:
- Math
- Date
- RegExp
- Array
- String
- Number
- Boolean
- Symbol
- BigInt
说几种数据去重的方法
1. [...new Set(arr)]
2. Array.from(new set(arr))
3. filter + indexOf
4. for + indexof 或 for + includes
说一说伪数组和数组的区别?
伪数组:
使用对象表示一个数组,与数组一样,具有索引(下标)和 length 属性。可以通过 for in 进行循环。
伪数组的类型是 Object,数组的类型是 Array
判断是否是数组:Array.isArray()
伪数组转化为数组:
1. [].slice.call 或者 Array.prototype.slice.call
2. Array.from
3. [...arguments]
索引不连续时转化结果是连续的,会自动补位。
常见的伪数组:
1. document.getElementsByClassName('right')
2. arguments
说一说 map 和 forEach 的区别?
map 和 forEach 都是es6新增的高阶函数,都是数组的方法,都有三个参数,用于数组遍历。
map 和 forEach 默认都不会改变原数组(不是绝对的,可以直接修改)。
map 会在内存中开辟一块新的空间,用来返回一个新的数组。forEach 返回值为 undefined。
性能方面:
for > forEach > map,因为 map 的返回值是一个等长的全新的数组,数组创建和赋值产生的性能开销很大。
说一说 ES6 中的箭头函数
1. 箭头函数没有 this,它内部的 this 指向的是上级作用域中的 this。
2. 它的写法简单。当只有一行返回值时,return 关键字 可以省略。
3. 不能作为构造函数,所以不能被 new。
4. 每一个函数都有 prototype 属性,但是箭头函数没有。
Set 和 WeakSet
- ES6 提供了新的数据结构 Set。它类似于数组,但是成员的值都是唯一的,没有重复的值。
- WeakSet 结构与 Set 类似,也是不重复的值的集合。但是 WeakSet 的成员只能是对象,而不能是其他类型的值。WeakSet 中的对象都是弱引用,这意味着如果除了 WeakSet 之外没有其他对某个对象的引用,那么这个对象可能会被垃圾回收器回收。
Map 和 WeakMap 结构?
- Map 数据结构。它类似于对象,也是键值对的集合,但是“键”的范围不限于字符串,各种类型的值(包括对象)都可以当作键。
- WeakMap 结构与 Map 结构类似,也是用于生成键值对的集合。但是 WeakMap 只接受对象作为键名( null 除外),不接受其他类型的值作为键名。WeakMap 中的对象都是弱引用,这意味着如果这个对象在其他地方没有被引用,那么它将会被垃圾回收,这也是 WeakMap 的主要应用场景。
扩展运算符使用场景
1. 伪数组转数组 [...arguments]
2. 合并剩余属性 const { name, age. ...rest } = record
3. 整合形参 function(...args) {}
4. 展开对象 const p = { name, age, ...record }
5. set 转数组 [...new Set(arr)]
说一说你对闭包的理解
形成条件:函数嵌套,内部函数引用外部函数的(变量或函数)。
解决的问题:内部的(变量或函数)会一直存在于内存中,直到内部函数成为垃圾对象才会被销毁,用于保存变量。
存在的问题:容易造成内存泄露。
闭包的应用:
-定义具有特定功能的 js 模块:
`- 将所有的数据和方法都封装到一个函数的内部(私有的)`
`- 只向外部暴露一个包含n个方法的对象或函数`
`- 模块的使用者,只需要通过模块导出的对象调用方法来实现对应的功能。`
- 防抖和节流
说一说变量提升和函数提升
变量提升指的是 JS 的变量和函数声明会在代码编译期间,提升到代码的最前面。
使用 var 声明的变量才会存在变量提升,只有声明提升,赋值不会提升,在 JS 中函数是一等公民,所以函数提升要优先于变量提升。
变量提升可以在变量初始化之前访问该变量,返回的是 undefined。在函数声明前可以提前调用该函数。
使用 let 和 const 声明的变量不会提升,形成暂时性死区,在初始化之前访问 变量会报错。
说一说 this 指向?(普通函数、箭头函数)
普通函数的 this 永远指向调用它的对象。
箭头函数:没有 this,它内部的 this 值指向上层作用域中的 this。
- fn():window 严格模式下 undefined。
- obj.fn(): 普通函数是 obj, 箭头函数是 window 或 undefined (看是否是严格模式)。
- var p = new Fu(): 普通函数是 p, 箭头函数不能被 new,直接报错。
- fn.call(obj):普通函数是 obj, 箭头函数是 window 或 undefined (看是否是严格模式)。
说一说 call apply bind 的作用和区别?
三者的作用都是改变函数执行时的 this 指向。
call 立即执行,参数(实际调用函数的实例对象,函数的参数...)
apply 立即执行,参数(实际调用函数的实例对象,函数的参数集合)
bind 返回一个新的函数,参数(实际调用函数的实例对象,函数的参数...)
bind 在 vue 与 react 框架中使用的较多,常用来改变函数运行时的 this 指向。
call 与 apply 在编写一些工具函数,如 防抖、节流中使用较多。
说一说 js 继承的方法和优缺点?
1. 原型链继承
将 子类 Cat 的 prototype 指向 父类 animal 的一个实例 (Cat.prototype = new Animal())
所有的属性都放在了原型上
缺点:
- 引用类型的属性被所有实例共享
- 在创建 Cat 的实例时,不能向 Animal 传参
// 将 子类 Cat 的 prototype 指向 父类 animal 的一个实例 (Cat.prototype = new Animal())
// 定义一个父类
function Animal(name, age) {
this.like = ['eat', 'drink', 'sleep']
}
// 为父类的原型上添加一个 run
Animal.prototype.run = function() {
console.log('run');
}
// 定义一个子类
function Cat() {
this.name = '小红'
}
// 核心:将 Cat 的原型指向父类 Animal 的一个实例
Cat.prototype = new Animal()
// 矫正 cat 的 constructor 属性
Cat.prototype.constructor = Cat
const cat = new Cat()
cat.run() // run
2. 借用构造函数(经典继承)
属性都放在了实例对象上
在子类构造函数中,调用父类构造函数方法,但通过
call
或者apply
方法改变了父类构造函数内this
的指向,使得子类实例出来的对象,自身拥有来自父类构造函数的方法跟属性,且分别独立,互不影响。
优点:
- 避免了引用类型的属性被所有实例共享
- 可以在 Child 中向 Parent 传参
缺点:
- 方法都在构造函数中定义,每次创建实例都会创建一遍方法。
function Animal(name, age) {
this.name = name
this.age = age
this.info = function() {
console.log(`${this.name}今年${this.age}岁了`);
}
}
function Cat(name, age) {
Animal.call(this, name, age)
}
const cat = new Cat('小红', 2)
const dog = new Cat('大黄', 3)
console.log(cat.info(), dog.info());
3. 组合继承(最常用)
优点:
- 1.融合原型链继承和构造函数的优点,是 JavaScript 中最常用的继承模式
function Animal(name, age) {
this.name = name
this.age = age
}
Animal.prototype.info = function() {
console.log(`${this.name}今年${this.age}岁了`);
}
function Cat(name, age) {
Animal.call(this, name, age)
}
Cat.prototype = new Animal()
Cat.prototype.constrcutor = Cat
const cat = new Cat('小红', 2)
const dog = new Cat('大黄', 3)
cat.info() // 小红今年2岁了
dog.info() // 大黄今年3岁了
4. 原型式继承
缺点:
- 1.包含引用类型的属性值始终都会共享相应的值,这点跟原型链继承一样。
// 定义一个父类(新建出来的对象的__proto__会指向它)
const Animal = {
name: 'nobody',
like: ['eat', 'drink', 'sleep'],
run() {
console.log('跑步');
}
}
// 新建以 Animal 为原型的实例
const cat = Object.create(
Animal,
// 这里定义的是实例自身的方法或属性
{
name: {
value: '小红'
}
}
)
cat.like.push('打豆豆')
// 新建以 Animal 为原型的实例
const dog = Object.create(
Animal,
// 这里定义的是实例自身的方法或属性
{
name: {
value: '大黄'
}
}
)
console.log(cat, dog);
5. 寄生式继承
// 它其实就是对原型式继承进行一个小封装,增强了一下实例出来的对象
const Animal = {
name: 'nobody',
like: ['eat', 'drink', 'sleep'],
run() {
console.log('跑步');
}
}
// 定义一个封装Object.create()方法的函数
const createObj = (parentProperty, ownProperty) => {
const obj = Object.create(parentProperty, ownProperty)
obj.catWalk = function() {
console.log('走猫步');
}
return obj
}
const cat = createObj(Animal, {
name: {
value: '小红'
}
})
console.log(cat, dog);
6. 寄生组合式继承
优点:
- 1.这种方式的高效率体现它只调用了一次 Parent 构造函数,并且因此避免了在 Parent.prototype 上面创建不必要的、多余的属性。
- 2.与此同时,原型链还能保持不变;
- 3.因此,还能够正常使用 instanceof 和 isPrototypeOf。开发人员普遍认为寄生组合式继承是引用类型最理想的继承范式
参考:segmentfault.com/a/119000000…
说一说 new 会发生什么?
// 1. 创建一个空对象
const obj = {}
// 2. 设置原型链
obj.__proto__ = constrcutor.prototype
// 3. 将构造函数作为对象的一个属性并执行获取结果
const key = Symbol()
obj[key] = constrcutor
const result = obj[key](...args)
// 4. 删除 obj 上的 函数
delete obj[key]
// 将结果返回
return typeof result === 'object' ? result : obj
说一说promise是什么与使用方法?
Promise 是一种异步编程的一种解决方案。支持 .then() 的链式调用,主要解决了异步编程所引起的回调地狱的问题。
Promise 是一个构造函数,接收一个函数作为参数,返回一个 Promise 实例。一个 Promise 实例有三种状态,分别是 pending、resolved 和 rejected,分别代表了进行中、已成功和已失败。实例的状态只能由 pending 转变 resolved 或者 rejected 状态,并且状态一经改变,无法再被改变了。状态的改变是通过 resolve() 和 reject() 函数来实现的,我们可以在异步操作结束后调用这两个函数改变 Promise 实例的状态,它的原型上定义了一个 then 方法,使用这个 then 方法可以为两个状态的改变注册回调函数。这个回调函数属于微任务,会在本轮事件循环的末尾执行。
说一说 js 实现异步的方法?
回调函数、
事件监听、
setTimeout、
Promise、
async/await、
生成器Generator、
说一说如何实现一个可过期的 localStorage 数据。
惰性删除:
特点:键值对过期后不会立马删除,下次使用时检查到过期才得到删除。
缺点:如果一个key一直没有被用到,即使它已经过期了也永远存放在 localStorage。
定时删除:
特点:每隔一段时间执行一次删除操作。
缺点:循环定时器会一直占据内存。
实现过程:
每隔一秒执行一次定时删除,操作如下:
1、随机测试20个设置了过期时间的key。使用正则匹配。
2、删除所有发现的已过期的key。
3、若删除的key超过5个则重复步骤1,直至重复500次。
说一下 token 能放在 cookie 中吗?
可以,cookie不设置过期时间就行,但是不推荐,因为无法防范 CSRF 攻击,并且 token 本身就有用于防范 CSRF 攻击的目的。token可以存放在Cookie中,token 是否过期,应该由后端来判断,不该前端来判断,所以token 存储在 cookie 中只要不设置 cookie 的过期时间就 ok 了,如果 token 失效,就让后端在接口中返回固定的状态表示token 失效,需要重新登录,再重新登录的时候,重新设置 cookie 中的 token 就行。
token 一般是用来判断用户是否登录的,它内部包含的信息有:uid(用户唯一的身份标识)、time(当前时间的时间戳)、sign(签名,token 的前几位以哈希算法压缩成的一定长度的十六进制字符串)
token认证流程:
- 客户端使用用户名跟密码请求登录
- 服务端收到请求,去验证用户名与密码
- 验证成功后,服务端签发一个 token ,并把它发送给客户端
- 客户端接收 token 以后会把它存储起来,比如放在 cookie 里或者 localStorage 里
- 客户端每次发送请求时都需要带着服务端签发的 token(把 token 放到 HTTP 的 Header 里)
- 服务端收到请求后,需要验证请求里带有的 token ,如验证成功则返回对应的数据
说一说创建 ajax 的过程
1、创建XHR对象:new XMLHttpRequest()
2、设置请求参数:XHR.open(Method, 服务器接口地址);
3、监听请求成功后的状态变化,根据状态码进行相应的处理。
4、发送请求: XHR.send(),如果是 get 请求不需要参数,post 请求需要参数 XHR.send(data)
readyState 值说明:
- 0:初始化,XHR 对象已经创建,还未执行 open
- 1:载入,已经调用 open 方法, 但是还没发送请求
- 2:载入完成,请求已经发送完成
- 3:交互,可以接收到部分数据
- 4:数据全部返回
status 值说明:
- 200:成功
- 404:没有发现文件、查询或URl
- 500:服务器产生内部错误
<button class="btn">点击</button>
<script>
function sendRequest() {
console.log('send');
const XHR = new XMLHttpRequest()
XHR.open('get', '/js.js', true)
XHR.onreadystatechange = function () {
if (this.readyState === 4 && XHR.status === 200) {
}
}
XHR.send()
}
document.querySelector('button.btn').onclick = sendRequest
</script>
说一下 fetch 的请求方式
fetch 是一种 HTTP 数据请求的方式,是 XMLHttpRequest 的一种替代方案,fetch 函数就是原生的js。
fetch 对象返回一个 promise,天生支持 promise。
XMLHttpRequest 的特点:
1. 所有的功能都集中在一个对象上,代码维护成本高并且比较混乱。
2. 不能适配 promise API。
fetch 的特点:
1. 精细的功能分割,头部信息、请求信息、响应信息都分布在不用的对象上。
2. 可以适配 Promise API。
3. 同源请求也可以自定义不带 Cookie,某些服务不需要 cookie 的话还能少一些流量。
说一下有什么方法可以保持前后端实时通信?
- websocket
- 短轮询:每隔一段时间客户端就发出一个请求,去获取服务器最新的数据。
- 长轮询:客户端向服务器发送请求,服务器接到请求后暂时不返回数据,直到有新消息才返回响应信息并关闭连接,客户端处理完响应信息后再向服务器发送新的请求。客户端可以设置一个超时时间,在超时时间之前服务器必须返回一次数据。
- iframe:在页面里嵌入一个隐蔵iframe,将这个隐蔵iframe的src属性设为对一个长连接的请求或是采用xhr请求,服务器端就能源源不断地往客户端输入数据。
说一说事件循环 Event Loop,宏任务与微任务?
- 整个 script 脚本作为一个宏任务进入主线程,代码自上而下,执行同步代码。。
- 遇到同步任务直接推入执行栈中执行,遇到异步任务,等待执行,异步任务结果有了结果,加入到对应的任务队列。
- 当前宏任务执行完毕后,会去微任务队列检查是否有微任务,如果有则依次执行,直到全部执行完毕。
- 执行完本轮的宏任务,检查宏任务队列并回到第 2 步继续执行此循环,直到宏任务与微任务队列都为空。
常见的宏任务有:
1. 整个 script 脚本。
2. setTimeout / setInterval
3. ajax请求
4. dom 事件回调
5. postMessage
微任务:
1. Promise.then() 的回调
js 获取原型的方法?
- p.proto
- p.contructor.prototype
- Object.getPrototypeOf(p)
typeof NaN 的结果是什么?
NaN 意指不是一个数字,(not a number),NaN 用于指出数字类型中的错误情况,执行数学运算没有成功,这事失败后的返回结果。
typeof NaN === ‘number’
NaN 不等于 NaN 它是唯一个和自身不相等的值。
eval 是做什么的?
它的功能是把对应的字符串解析成 JS 代码并运行。
应该避免使用 eval,不安全,非常耗性能(2次,一次解析成 js 语句,一次执行)。
js 的几种模块化规范?
js 中现在比较成熟的有四种模块加载方案。
第一种是 CommonJS 方案,它通过 require 来引入模块,通过 module.exports 定义模块的输出接口。这种模块加载方案是服务器端的解决方案,它是以同步的方式来引入模块的,因为在服务端文件都存储在本地磁盘,所以读取非常快,所以以同步的方式加载没有问题。但如果是在浏览器端,由于模块的加载是使用网络请求,因此使用异步加载的方式更加合适。
第二种是 AMD 方案,这种方案采用异步加载的方式来加载模块,模块的加载不影响后面语句的执行,所有依赖这个模块的语句都定义在一个回调函数里,等到加载完成后再执行回调函数。require.js 实现了 AMD 规范。
第三种是 CMD 方案,这种方案和 AMD 方案都是为了解决异步模块加载的问题,sea.js 实现了 CMD 规范。它和 require.js的区别在于模块定义时对依赖的处理不同和对依赖模块的执行时机的处理不同。
第四种方案是 ES6 提出的方案,使用 import 和 export 的形式来导入导出模块。这种方案和上面三种方案都不同。
js 中倒计时的纠偏实现?
在前端实现中我们一般通过 setTimeout 和 setInterval 方法来实现一个倒计时效果。但是使用这些方法会存在时间偏差的问题,这是由于 js 的程序执行机制造成的,setTimeout 和 setInterval 的作用是隔一段时间将回调事件加入到事件队列中,因此事件并不是立即执行的,它会等到当前执行栈为空的时候再取出事件执行,因此事件等待执行的时间就是造成误差的原因。
一般解决倒计时中的误差的有这样两种办法:
(1)第一种是通过前端定时向服务器发送请求获取最新的时间差,以此来校准倒计时时间。
(2)第二种方法是前端根据偏差时间来自动调整间隔时间的方式来实现的。这一种方式首先是以 setTimeout 递归的方式来实现倒计时,然后通过一个变量来记录已经倒计时的秒数。每一次函数调用的时候,首先将变量加一,然后根据这个变量和每次的间隔时间,我们就可以计算出此时无偏差时应该显示的时间。然后将当前的真实时间与这个时间相减,这样我们就可以得到时间的偏差大小,因此我们在设置下一个定时器的间隔大小的时候,我们就从间隔时间中减去这个偏差大小,以此来实现由于程序执行所造成的时间误差的纠正。
let 和 const 的区别?
- 1.声明的变量只在声明时的代码块内有效
- 2.不存在声明提升
- 3.存在暂时性死区,如果在变量声明前使用,会报错
- 4.不允许重复声明,重复声明会报错
什么是 rest 参数?
rest 参数(形式为...变量名),用于获取函数的多余参数。
什么是尾调用,尾调用的优势是什么?
尾调用指的是函数的最后一步调用另一个函数。我们代码执行是基于执行栈的,所以当我们在一个函数里调用另一个函数时,
我们会保留当前的执行上下文,然后再新建另外一个执行上下文加入栈中。使用尾调用的话,因为已经是函数的最后一步,所以这个时候我们可以不必再保留当前的执行上下文,从而节省了内存,这就是尾调用优化。但是 ES6 的尾调用优化只在严格模式下开启,正常模式是无效的。