1. js有哪几种数据类型?如何判断数据的类型?
基本类型:Number, String, Boolean, null, undefined, Symbol(ES6新增)
引用类型:数组,对象,函数(都是对象类型)
使用typeof
来判断,有局限性。数组、对象用typeof判断都是object类型
使用Object.prototype.toString.call()
来判断,比较完美
typeof NaN // "number"
typeof function() {} // "function"
typeof [] // "object"
typeof {} // "object"
Object.prototype.toString.call(NaN) // "[object Number]"
Object.prototype.toString.call([]) // "[object Array]"
2. js创建对象的几种方式
var obj = {}
var obj = new Object()
var obj = Object.create(Object.prototype) // Object.create(obj) 创建一个新对象,新对象的__proto__为obj
3. 深浅拷贝
常见的拷贝对象方法如Object.assign(obj, obj1)
,扩展运算符...
都是浅拷贝
JSON.parse(JSON.stringify(obj))
是深拷贝。但当obj为undefined或者函数时,会报错
深拷贝可以使用lodash库的cloneDeep(obj)
方法
或者自己封装一个深拷贝函数。主要实现原理就是:如果是基本数据类型,直接复制即可。如果是引用类型,就遍历所有项,如果是基本类型,直接复制;如果是引用类型,执行递归操作。
具体实现见另一篇文章:JavaScript深浅拷贝
4. 变量提升
函数声明与变量声明经常被JavaScript引擎隐式地提升到当前作用域的顶部
如果变量名与函数名同名,则函数声明会覆盖变量声明
变量提升的只是声明部分,不包括赋值部分
console.log(fun) // 打印结果为: f fun() { console.log(2) }
var fun = 3
function fun() {
console.log(2)
}
5. 作用域链
js变量作用域分函数作用域和全局作用域。
函数执行时,从当前作用域开始搜,没有找到的变量,会向上层查找,直至全局函数。这就是作用域链。
6. 闭包
函数嵌套,内部函数能够访问外部函数的局部变量。
function fun() {
var count = 2
return function() {
console.log(count)
}
}
fun()
优点:长外部函数局部变量生命周期
缺点:容易造成内存泄漏,被内部函数访问的变量需要手动回收
7. this指向
普通函数,this指向调用它的那个对象
箭头函数,没有具体的this,它的this相当于是从上下文继承的,也就是说定义时候的上下文this.(使用call,apply等任何方式都无法改变this的指向)
8. call, apply, bind
call, apply和bind是Function原型上的三个方法,都是为了改变函数体内部this的指向。
call、apply、bind 三者第一个参数都是this要指向的对象,后面的参数call是一个个的参数列表,apply则是放到数组中的
bind 是返回一个函数,便于稍后调用。call, apply则是立即调用
9. 原型链
每个函数都有一个prototype
属性,是一个对象,我们称之为原型对象。原型对象有个constructor
属性,指向这个函数。
prototype上的属性和方法可以被实例对象调用。
prototype还可以用来实现继承,也就是原型链继承。 只需要将子类构造函数的原型指向父类的一个实例
每个对象都有__proto__
属性,指向其构造函数的原型对象。当我们使用对象的属性时候,如果本身不包含某个属性,就会到其构造函数的原型上查找,而构造函数的原型也是对象,也可以向上查找,直到null为止
10. js如何实现继承
// 构造函数继承
function Student() {
Person.call(this) // 只能继承父类构造函数里面的属性,不能继承原型上的属性和方法
}
// 原型链继承
function Student() {}
Student.prototype = new Person() // 引用类型的原型属性会被所有的实例共享
// 组合继承,二者结合
function Student() {
Person.call(this);
}
Student.prototype = new Person() // 调用了两次父类构造函数,生成了两份实例(子类实例的属性,在子类实例的__proto__中也存在)
// 寄生组合继承
function Student() {
Person.call(this)
}
(function(){
let Super = function() {}
Super.prototype = Person.prototype
Student.prototype = new Super() // 比较完美,但时间较为复杂
})()
// class继承
class Student extend Person() {}
11. new操作符具体干了什么
创建一个空对象
由this变量引用该对象
该对象继承构造函数的原型(更改this指向)
把属性和方法加入到this引用的对象
最后隐式的返回this
let obj = {}
obj.__proto__ = Person.prototype
Person.call(obj)
12. js如何实现一个类?
// 使用构造函数,方法需要写在构造函数的原型链上
function Person(name, age) {
this.name = name
this.age = age
}
Person.prototype.say = function() {
console.log(this.name)
}
var person = new Person('zs', 18)
person.say()
// 使用class
class Person {
constructor(name, age) {
this.name = name
this.age = age
}
say() {
console.log(this.name)
}
}
var person = new Person('zs', 18)
person.say()
13. 事件队列
js执行时,遇到同步任务,就将同步任务按照执行顺序排列到执行栈中。
遇到异步任务,会将此类异步任务挂起,继续执行执行栈中的任务。等异步任务返回结果后,再按照顺序排列到事件队列中。
主线程先将执行栈中的同步任务清空,然后检查事件队列中是否有任务,如果有,就将第一个事件对应的回调推到执行栈中执行,若在执行过程中遇到异步任务,则继续将这个异步任务排列到事件队列中。
主线程每次将执行栈清空后,就去事件队列中检查是否有任务,如果有,就每次取出一个推到执行栈中执行,这个循环往复的过程被称为“Event Loop 事件循环”。
14. 宏任务与微任务
(1) 宏任务:setTimeout(), setInterval(), requireAnimationFrame()
宏任务队列所处的队列就是宏任务队列
第一个宏任务队列中只有一个任务:执行主线程js代码
宏任务队列可以有多个
(2) 微任务:new Promise().then(), process.nextTick()
微任务所处的队列就是微任务队列
只有一个微任务队列
上一个宏任务队列执行完毕后,如果有微任务队列,就会执行微任务队列里的所有任务
15. js事件传播机制
js事件传播有三个阶段:事件捕获、目标阶段、事件冒泡
事件捕获:从外到内进行事件传播
目标阶段:事件传播到事件目标
事件冒泡:从内到外进行事件传播
一般情况下,默认都是冒泡阶段触发事件,因此事件触发的顺序是从内到外。
取消默认事件:W3C的方法是e.preventDefault(),IE则是使用e.returnValue = false
16. 什么是事件代理?它有什么好处?
利用事件冒泡的原理,把事件加到父级上,触发执行效果
好处:减少事件数量,提高性能
新添加的元素,依然可以触发该事件
17. 如何阻止事件冒泡
阻止冒泡:W3C的方法时ev.stopPropagation(), 早期IE的方法是ev.cancelBubble = true。
需要注意的是ev.stopPropagation()是用来阻止事件传播的,也可以在捕获阶段使用。
18. cookies与session
cookie、session都是用来记录用户状态的。不同的是cookie保存在客户端,session保存在服务器。
具体来说,用户登录网站,服务器会通过response给客户端一个cookie。
客户端浏览器会把cookie存起来,等下次再次请求该网站的时候,会把请求的url和cookie一同提交给服务器。
服务器检查该Cookie, 以此来辨认用户状态。 服务器还可以根据需要修改Cookie的内容。
session保存在服务器上。 客户端浏览器访问服务器的时候, 服务器把客户端信息以某种形式记录在服务器上。
这就是session。 客户端浏览器再次访问时只需要从该session中查找该客户的状态就可以了。
19. localstorage与sessionstorage
localStorage和sessionStorage一样都是用来存储客户端临时信息的对象。
sessionStorage用于本地存储一个会话(session)中的数据,这些数据只有在同一个会话中的页面才能访问
并且当会话结束后数据也随之销毁。因此sessionStorage不是一种持久化的本地存储,仅、是会话级别的存储。只允许同一窗口访问。
而localStorage用于持久化的本地存储,除非主动删除数据,否则数据是永远不会过期的。同源可以读取并修改localStorage数据。
20. Proxy与Reflect
Proxy 对象用于创建一个对象的代理, 从而实现基本操作的拦截和自定义
Reflect 是一个内置的对象, 它提供拦截 JavaScript 操作的方法。
Reflect并非一个构造函数, 所以不能通过new运算符对其进行调用,
或者将Reflect对象作为一个函数来调用。 Reflect的所有属性和方法都是静态的( 就像Math对象)。
21. js垃圾回收机制知道哪些
标记清除:垃圾回收期会在运行的时候给存储在内存中的所有变量加上标记,然后去掉环境中的变量,以及被环境中变量引用的变量(闭包)的标记。在完成这些后依然存在的标记就是要删除的变量。
引用计数:引用计数的策略是跟踪记录每个值被使用的次数。当声明了一个变量,并将一个引用类型赋值给该变量时,这个值的引用次数就加1。如果该变量的值变成了另外一个,则这个值的引用次数减1。当这个值的引用次数为0的时候,就可以将它占用的空间回收。
IE中JavaScript对象通过标记清除的方式就行垃圾回收,但是BOM和DOM对象是用引用技术的方式回收的。也就是说,只要是涉及BOM和DOM,就有可能出现循环引用问题。
22. 节流和防抖
// 节流函数
// 应用场景: 表单提交(频繁点击按钮,只保存一次)
function throttle(fn, delay) {
var lastTime = 0
return function() {
var nowTime = Date.now()
if (nowTime - lastTime > delay) {
fn()
lastTime = nowTime
}
}
}
// 防抖函数
// 应用场景:邮件校验,输入框模糊查询(等输入完了在执行查询)
function debounce(fn, delay) {
var timer = null
return function() {
clearTimeout(timer)
timer = setTimeout(fn, 500)
}
}
23. script标签中defer与async的区别
没有defer或async属性,浏览器会立即下载并执行相应的脚本,并且在下载和执行时页面的处理会停止。
有了defer属性,浏览器会立即下载相应的脚本,在下载的过程中页面的处理不会停止,等到文档解析完成后脚本才会执行。
有了async属性,浏览器会立即下载相应的脚本,在下载的过程中页面的处理不会停止,下载完成后立即执行,执行过程中页面处理会停止。
如果同时指定了两个属性,则会遵从async属性而忽略defer属性。
24. 什么是内存泄漏,哪些操作会造成内存泄漏?
内存泄漏指的是不再使用或需要的数据,仍然存在于内存中。
以下操作会造成内存泄漏:
setTimeout的第一个参数使用字符串而不是函数
闭包
控制台日志
对象循环引用(两个对象彼此引用且彼此保留,会产生一个循环)
25. let、const、var
let声明后会形成一个封闭作用域,使得变量只在这个块作用域内有效
let不存在变量提升,不可重复声明。
const代表是一个常量,一旦声明,必须初始化。同样会形成一个封闭作用域。
const 保证的不是变量的值不变,而是变量指向的内存地址的数据不允许改动。对于引用类型(对象 object,数组 array,函数 function),变量指向的内存地址其实是保存了一个指向实际数据的指针,所以 const 只能保证指针是固定的,至于指针指向的数据就无法控制了,所以使用 const 声明引用类型时要慎重。
26. Promise是什么,Promise作用?
Promise是异步编程的一种解决方案。
具体表达:
语法上来说,Promise是一个构造函数
功能上来说,Promsie对象用来封装一个异步操作并获取其结果
Promise作用:
指定回调函数的方式更加灵活
回调函数:必须在启动异步任务前指定
Promise: 启动异步任务,返回Promise对象,给Promise对象绑定回调函数(可以在异步任务结束后执行)
支持链式调用,解决回调地狱问题。
回调地狱:不便于阅读,不便于异常处理
终极解决方案:async...await(完全没有回调函数)
关于Promise,可以参考另一篇文章:自定义Promise
27. async...await
async...await,是异步程的一种解决方案,与Promise有很大关联。
async函数返回一个Promise对象。可以使用then方法添加回调函数。
await用于等待一个Promise对象,只能在async函数内部使用。
await语句的返回值跟await后的表达式相关。
表达式为Promise对象:await会暂停执行,等待Promise对象resolve,然后返回解析值并回复async函数的执行。
非Promise对象:直接返回对应的值。
关于async...await,可以参考另一篇文章:async...await
28. Iterator作用,它的遍历过程是什么
Iterator为不同的数据结构提供统一的访问机制。部署了Iterator接口的数据结构能够使用for...of来进行遍历。
数组、Set和Map原生具备Iterator接口,不用任何处理,可以被for...of遍历。原因在于,它们部署了Symbol.iterator属性。
凡是部署了Symbol.iterator属性的数据结构,就称为部署了遍历器接口。
Iterator 的遍历过程:
(1)创建一个指针对象,指向当前数据结构的起始位置。也就是说,遍历器对象本质上,就是一个指针对象。
(2)第一次调用指针对象的next方法,可以将指针指向数据结构的第一个成员。
(3)第二次调用指针对象的next方法,指针就指向数据结构的第二个成员。
(4)不断调用指针对象的next方法,直到它指向数据结构的结束位置。
29. 说说对模块化开发的认识
30. ES6对象新增的方法
Object.is()
比较两个值是否相等,与严格的===基本一致。不同之处只有两个,如下:
Object.is(+0, -0) // false
Object.is(NaN, NaN) // true
+0 === -0 // true
NaN === NaN // false
Object.assign()
用于对象的合并
Object.keys()
返回对象的键组成的数组
Object.values()
返回对象的值组成的数组
Object.entries()
返回一个数组,成员是键值对组成的数组
const obj = { a: 1, b: 2 }
Object.entries(obj) // [['a', 1], ['b', 2]]
Object.fromEntries()
是Object.entries()
的逆操作,用于将一个键值对数组转换为对象
31. ES6数组新增的方法
Array.from()
将两类对象转为真正的数组:类似数组的对象(array-like object)和可遍历(iterable)的对象(包括 ES6 新增的数据结构 Set 和 Map)
Array.of() 将一组值,转换为数组
Array.prototype.find()
找出第一个符合条件的数组成员,如果没有,返回undefined
Array.prototype.findIndex()
返回第一个符合条件的数组成员的位置,如果所有成员都不符合条件,则返回-1。
Array.prototype.fill()
Array.prototype.includes()
数组是否包含某个元素
Array.prototype.flat()
数组扁平化
entries(),keys()和values()用于遍历数组,他们都返回一个遍历器对象。keys()是对键名的遍历,values()是对值的遍历,entries()是对键值对的遍历。
Array.from(new Set([1, 2])) // [1, 2]
Array.of(1, 2, 3) // [1, 2, 3]
new Array(4).fill('1') // [1, 1, 1, 1]
[1, 2, [3, 4]].flat() // [1, 2, 3, 4]
32. ES11新特性
(1)?.链合并运算符
let person = {
info: {
name: 'Jack',
age: 18
}
}
let name = person?.info?.name
(2)??null判断运算符,只有左侧为null或者undefined时,才会返回右侧的值
let name = person?.info?.name ?? 'Jack'
(3)动态导入import(),适用于按需加载,条件加载
返回一个Promise对象,加载成功后,模块会作为一个对象,当做then回调的参数
(4)bigint js只能精确到53个二级制位置
typeof 1n // bigint
(5)Promise.allSettled()接收一组Promise作为参数,必须等所有的promise返回结果后,才会结束
(6)String.prototype.matchAll()返回一个正则表达式在当前字符串中所有的匹配
(7)globalThis 获取顶层对象,无论实在浏览器还是Node里
33. 基本类型与引用类型区别。为什么引用类型存储在堆里?
基本类型存储在栈中。
引用类型会在栈中保存一个引用(指针),实际内容存储在堆中。当我们修改一个引用内容的时候,其他引用这块内容的变量,结果也会发生变化。
就查询速度而言,栈远快于堆
stack创建时候,大小是确定的,超过额度大小就会发生栈溢出【当js出现死循环或者错误的递归时候】。
heap大小是不确定的,需要可以一直累加。
参考资料:
2020前端基础包过面试题
script标签中的defer和async属性