一、JS的数据类型以及它们的区别
JS的数据类型分为:基础数据类型和引用数据类型。
- 基础数据类型:Number、Boolean、String、Undefined、Null,以及ES6新增的Symbol、BigInt。
- 引用数据类型:Object
其中,Symbol和BigInt是ES6新增的数据类型:
- Symbol:代表创建后独一无二且不可改变的数据类型,它主要是为了解决可能出现的全局变量冲突的问题。
- BigInt:一种数字类型的数据。它可以表示任何精度格式的整数,使用BigInt可以安全存储和操作大整数,即使这个数已经超出了Number能够表示的安全整数范围。使用BigInt可以避免精度丢失。
二、数据类型检测的方式有哪些
- typeof
console.log(typeof 1) // number
console.log(typeof 'a') // string
console.log(typeof true) // boolean
console.log(typeof undefiend) // undefiend
console.log(typeof null) // object
console.log(typeof {}) // object
console.log(typeof []) // object
其中,null、对象、数组的数据类型判断为object,其他判断正确。
typeof null === 'object'原因:
- JavaScript早期设计失误,null底层二进制存储为000,与object相似,导致
typeof null === 'object'。
- instanceof
instanceof可以正确判断对象数据类型,其内部运行机制是判断在其原型链中能否找到该类型的原型。
console.log(1 instanceof Number) // false
console.log('a' instanceof String) // false
console.log(true instanceof Boolean) // false
console.log({} instanceof Object) // true
console.log([] instanceof Array) // true
console.log(function() {} instanceof Function) // true
instanceof只能判断引用数据类型,不能判断基本数据类型。
- constructor
console.log((1).constructor === Number) // true
console.log(('a').constructor === String) // true
console.log((true).constructor === Boolean) // true
console.log(({}).constructor === Object) // true
console.log(([]).constructor === Array) // true
console.log((function(){}).constructor === Function) // true
constructor有两个作用,一是判断数据的类型,二是对象实例通过constructor对象访问对象的构造函数。需要注意,如果创建一个对象来改变它的原型,constructor就不能用来判断数据类型了
- Object.prototype.toString.call()
const a = Object.prototype.toString
console.log(a.call(1))
console.log(a.call(true))
console.log(a.call('a'))
console.log(a.call([]))
console.log(a.call(function(){}))
console.log(a.call({}))
console.log(a.call(undefined))
console.log(a.call(null))
Object.prototype.toString.call()可以检测所有数据类型。
同样是检测对象obj调用toString方法,obj.toString()的结果和Object.prototype.toString.call(obj)的结果不一样,这是为什么?
因为toString是Object原型上的方法,向Array、Function(){}等类型作为Object上的实例,都重写了toString方法。不同的对象调用toString方法,调用的都是重写的toString方法,不会去调用Object原型上的方法。
三、js的执行上下文
四、请简述js中的this
this是JavaScript中的上下文对象,它指向最后一次调用这个方法的对象。
常见的this使用情况:
- 在普通函数中,this指向全局对象(在浏览器中指向window,在nodejs中指向global)
- 在函数作为对象的方法使用时,this指向该对象
- 箭头函数中的this,箭头函数没有自己的this,它的this永远指向它的作用域父级,且this指向不可变
- 构造函数中的this,指向实例对象
call、apply和bind方法,可以改变this指向
五、箭头函数与普通函数的区别
- 箭头函数更加简洁
const fn = () => {} //箭头函数
function fn() {} //普通函数
- 箭头函数没有自己的this,它的this继承于它的上级作用域
- 箭头函数继承来的this指向永远不会被改变
- call、apply、bind不会改变箭头函数的this指向
- 箭头函数不能作为构造函数使用
- 箭头函数没有自己的
arguments - 箭头函数没有
prototype - 箭头函数不能作为
Generator生成器函数,不能使用yeild关键字
六、js的模块化
js的模块化概念:JS 模块化是为了解决作用域污染、依赖混乱和可维护性差的问题。随着项目变大,把代码拆成模块能让我们更好地管理依赖关系,减少全局变量冲突,也方便团队协作。
模块化也为构建工具提供了基础,比如 tree-shaking、按需加载等优化手段。
js的模块化分为:AMD、CommonJS、ES Module
AMD概念:AMD(Asynchronous Module Definition)是一种浏览器端的模块化规范,由 RequireJS 推广,强调异步加载模块。
CommonJS概念:CommonJS 是一种模块化规范,最初是为服务器端(Node.js)设计的,强调同步加载模块。
AMD和CommonJS的区别
- AMD是异步加载,适用于浏览器端
- CommonJS是同步加载,适用于nodejs
七、let、const、var区别
- 变量提升:var具有变量提升,let、const不存在变量提升
- 暂时性死区:let、const具有暂时性死区,var不存在暂时性死区
- 块级作用域:块级作用域是由
{}包裹,let、const具有块级作用域,var没有块级作用域。 - 给全局添加属性:浏览器的全局对象是
window,node的全局对象是global。var声明的变量为全局变量,并会将该变量添加到全局对象的属性上。但是let、const不会。 - 重复声明:var声明变量时,可以重复声明变量,后面声明的同名变量会覆盖前面的。let、const不允许重复声明。
- 初始值设置:const声明必须赋值,let、var可以不用设置初始值。
- 指针指向:var、let创建的变量可以改变指针指向(重新赋值),const声明的变量不可以改变指针指向。
八、块级作用域解决了ES5中的什么问题
- 解决了变量提升的问题:内层变量可以覆盖外层变量
var x = 10 function fn() { console.log(x) // undefined var x = 20 console.log(x) // 20 } fn() console.log(x) // 10 // fn()函数经过变量提升之后 function fn() { var x console.log(x) x = 20 console.log(x) } - 解决了变量覆盖问题
var x = 10 if (true) { var x = 20 // 重新赋值 } console.log(x) // 20 - for循环变量共享的问题
var没有块级作用域,i 变成了全局变量。
for(var i = 0; i < 3; i++){ setTimeOut(function() { console.log(i) }, 0) } // 预期结果: 0,1,2 // 实际结果: 3,3,3let具有块级作用域,每次循环都会创建一个新的i,setTimeout中的i互不影响。
for(let i = 0; i < 3; i++){ setTimeOut(function() { console.log(i) }, 0) } // 实际结果: 0,1,2 - 变量重复声明的问题:var创建的变量可以重复声明,容易引发问题。let、const禁止重复声明变量。
var x = 10 var x = 20 // 重复声明不会报错 console.log(x) // 20
九、new操作符的实现原理
- 新建一个空对象
- 将空对象的原型挂载到构造函数的原型上
- 将空对象的this指向构造函数
- 返回这个对象
// 实现
function myNew(constructor, ...args) {
if (typeof constructor !== 'function') return
let obj = {}
obj.__proto__ = constructor.prototype
const result = constructor.apply(obj, args)
return result instanceof Object ? result : obj
}
// 使用实例 - 构造函数没有显式返回值
function Person(name, age) {
this.name = name;
this.age = age;
}
const person1 = new Person('lili', 24)
console.log(person1) // 打印结果 {name: 'lili', age: 24}
const person2 = myNew(Person, 'qiqi', 21)
console.log(person2) // 打印结果 {name: 'qiqi', age: 21}
console.log(person1 instanceof Person); // 打印结果 true
console.log(person2 instanceof Person); // 打印结果 true
// 使用实例 - 构造函数有显式返回值
function Fn(name, age, sex) {
this.name = name
this.age = age
return { sex }
}
const fn1 = new Fn('lili', 24, '女')
console.log(fn1.name) // 打印结果 undefined
console.log(fn1) // 打印结果 {sex: '女'}
console.log(fn1.sex) // 打印结果 '女'
const fn2 = myNew(Fn, 'qiqi', 21, '男')
console.log(fn2) // 打印结果 {sex: '男'}
console.log(fn2 instanceof Fn) // 打印结果 false
十、for in和for of的区别
- for in 遍历的是对象的键名(key)
- for of 遍历的是对象的键值(value)
十一、原型和原型链
当访问对象上的一个属性时,如果在该对象身上没有找到该属性,通常会去该对象的原型上找,该原型对象又会有自己的原型,这样一层一层的向上寻找,形成了原型链。原型链的尽头一般是Object.prototype。
十二、事件循环(EventLoop)
事件循环是js执行异步代码的机制,它保证了js是单线程的,也能执行异步任务不阻塞主任务。
- 首先会先执行
同步任务,再执行异步任务,进入到事件循环过程中。 - 执行
异步任务的时候又会区分微任务和宏任务。先执行微任务。 - 后执行
宏任务,如果执行宏任务的过程中产生微任务的话,会把微任务放到微任务队列中。 - 执行完
当前宏任务,立刻执行微任务,再执行下一轮宏任务。
- 同步任务:console.log()、变量声明、函数调用、for循环
- 异步任务:
setTimeout、fetch、事件绑定(click)、Promise、DOM 渲染 - 微任务:
Promise.then/catch/finally、Async/AwaitMutationObserver监听 DOM 变动(浏览器原生)queueMicrotask(fn)手动添加微任务(浏览器和 Node 支持)process.nextTickNode.js 中的微任务,比 Promise 更早
- 宏任务:
setTimeout(fn, 0)定时器setInterval(fn, 1000)定时重复任务setImmediate(fn)Node.js 特有,立即执行axios网络请求
十三、闭包
- 闭包:指在函数内部可以访问到函数外部的变量。
- 在哪里使用过闭包:防抖、节流函数。
十四、Promise
Promise的概念:Promise是ES6新引入的异步编程解决方案。
Promise的状态:
- pending 进行中
- fulfilled 已成功
- rejected 已失败
状态只能从 pending 变为 fulfilled或 rejected,一旦变更不可逆。
Promise的方法:
Promise.all: 可以放多个promise对象,会等所有promise对象都执行完成后一起返回。如果其中有一个promise对象执行失败,promise.all返回失败结果。Promise.race: race的意思是“赛跑”。它里面可以放多个promise对象,哪个promise对象先执行完成,先返回哪个promise对象。
十五、异步编程的实现方式
- Promise
- Async/Await
- 回调函数
- generator(生成器)函数
十六、内存泄露
- 意外的全局变量
- 被遗忘的计时器和回调函数
- 不正确的使用闭包
- 脱离DOM的引用
十七、ES6的新特性
- 箭头函数
- 新的变量声明方式let、const
- 解构赋值
const [a, b] = [1, 2] - promise
- 模版字符串: 在代码层面可换行
const a = 'str'
console.log(`<p>${a}</p>
<div>${a}</div>`)
- 扩展运算符
const a = [1, 2, 3]
const b = [5, 6, 7]
const c = [...a, ...b] // [1, 2, 3, 5, 6, 7]
- Symbol数据类型,代表独一无二的值
- 模块化
- for...of 遍历对象的键值
- Map 和 Set复杂数据类型:
- Proxy代理
- 类(Class):面向对象编程的概念
- 默认参数(Default Parameter),在定义函数时可以给参数设置默认值
十八、数组
七个可以改变原数组的数组方法
unshift()、shift()、push()、pop()、sort()、reverse()、splice()
数组的其他常见方法
concat()、foreach()、map()、some()、every()
数组转换成字符串
- toString() 默认按逗号分隔
["hello", "world"].toString()→"hello,world" - join() 将数组元素用指定分隔符连接成字符串
["a", "b", "c"].join("-")→"a-b-c"
数组foreach方法和map方法的区别
- forEach遍历数组执行副作用操作,没有返回值;map会返回一个新数组,常用于对数组进行映射转换;
- map可以支持链式调用,更适合函数式编程
数组foreach方法和map方法的相同点
都不会改变原数组
forEach方法可以主动中断吗
不可以,break/return/continue都不生效。
判断数组的方法有哪些
- 通过instanceof判断
[] instanceof Array Object.prototype.toString.call([])- 通过ES6的
Array.isArray([])判断 - 通过原型链判断
[].__proto__ === Array.prototype - 通过
Array.prototype.isPrototypeOf([])判断
十九、对象
对象的静态方法
Object 的静态方法主要用于对象的创建、拷贝、属性定义、遍历等,常见的有 assign、keys、values、entries、create、defineProperty、freeze、is 等等。
获取属性相关
对象方法示例表格
| 方法名 | 作用 | 示例 | 输出结果 |
|---|---|---|---|
Object.keys(obj) | 获取可枚举自身属性名数组 | Object.keys({ a: 1, b: 2, c: 3 }) | ['a', 'b', 'c'] |
Object.values(obj) | 获取可枚举自身属性值数组 | Object.values({ a: 1, b: 2, c: 3 }) | [1, 2, 3] |
Object.entries(obj) | 获取可枚举自身属性的键值对 | Object.entries({ a: 1, b: 2 }) | [['a', 1], ['b', 2]] |
Object.getOwnPropertyNames(obj) | 获取所有自身属性名(包括不可枚举) | Object.getOwnPropertyNames({ a: 1, b: 2 }) | ['a', 'b'] |
Object.getOwnPropertySymbols(obj) | 获取所有 Symbol 类型属性名 | const sym = Symbol('id'); const obj = { [sym]: 123, a: 1 }; Object.getOwnPropertySymbols(obj) | [Symbol(id)] |
判断类
| 方法名 | 作用 | 示例 | 输出结果 |
|---|---|---|---|
Object.is(val1, val2) | 判断是否“严格相等”(类似 ===,但处理 NaN 和 -0 更精准) | Object.is(NaN, NaN) Object.is(0, -0) Object.is(1, 1) | true false true |
Object.hasOwn(obj, key) | 检查对象自身是否含有某属性(ES2022,推荐) | const obj = { a: 1 }; Object.hasOwn(obj, 'a') Object.hasOwn(obj, 'toString') | true false |
创建类
| 方法名 | 作用 | 示例 | 输出结果 |
|---|---|---|---|
Object.create(proto) | 以指定原型创建新对象 | const proto = { greet() { return 'hello' } }; const obj = Object.create(proto); obj.name = '张三'; obj.greet() | 对象 { name: '张三' } 原型指向 proto 'hello' |
Object.assign(target, ...) | 浅拷贝、合并对象 | const target = { a: 1 }; const source = { b: 2, c: 3 }; Object.assign(target, source) console.log(target) | { a: 1, b: 2, c: 3 } |
Object.fromEntries(entries) | 把键值对数组转成对象(ES10) | const entries = [['a', 1], ['b', 2], ['c', 3]]; Object.fromEntries(entries) | { a: 1, b: 2, c: 3 } |
属性操作类
| 方法名 | 作用 | 示例 | 输出结果 |
|---|---|---|---|
Object.defineProperty(obj, key, descriptor) | 定义单个属性(可控制 writable, enumerable 等) | const obj = {}; Object.defineProperty(obj, 'name', { value: '张三', writable: false, enumerable: true }); obj.name = '李四'; console.log(obj.name) | 属性定义成功 修改无效 ,因为 writable: false, 输出 '张三' |
Object.defineProperties(obj, descriptors) | 批量定义属性 | const obj = {}; Object.defineProperties(obj, { name: { value: '张三', writable: true }, age: { value: 18, writable: false } }); console.log(obj.name, obj.age) | '张三' 18 |
Object.getOwnPropertyDescriptor(obj, key) | 获取某属性的描述符 | const obj = { name: '张三' }; Object.getOwnPropertyDescriptor(obj, 'name') | { value: '张三', writable: true, enumerable: true, configurable: true } |
Object.getOwnPropertyDescriptors(obj) | 获取所有属性的描述符(ES8) | const obj = { name: '张三', age: 18 }; Object.getOwnPropertyDescriptors(obj) | { name: {...}, age: {...} } |
控制对象状态类
| 方法名 | 作用 | 示例 | 输出结果 |
|---|---|---|---|
Object.freeze(obj) | 冻结对象(不可增删改) | const obj = { name: '张三' }; Object.freeze(obj); obj.name = '李四'; obj.age = 18; delete obj.name; | 冻结对象 修改无效 新增无效 删除无效 |
Object.seal(obj) | 密封对象(不可新增,可改值) | const obj = { name: '张三' }; Object.seal(obj); obj.name = '李四'; obj.age = 18; delete obj.name; | 密封对象 修改成功 '李四' 新增无效 删除无效 |
Object.preventExtensions(obj) | 禁止新增属性 | const obj = { name: '张三' }; Object.preventExtensions(obj); obj.age = 18; console.log(obj.age) | 禁止扩展 新增无效 undefined |
Object.isFrozen(obj) | 检查是否被冻结 | const obj = { name: '张三' }; Object.freeze(obj); Object.isFrozen(obj) | true |
Object.isSealed(obj) | 检查是否被密封 | const obj = { name: '张三' }; Object.seal(obj); Object.isSealed(obj) | true |
Object.isExtensible(obj) | 检查是否可扩展 | const obj = { name: '张三' }; Object.preventExtensions(obj); Object.isExtensible(obj) | false |
原型链操作类
| 方法名 | 作用 | 示例 | 输出结果 |
|---|---|---|---|
Object.getPrototypeOf(obj) | 获取对象原型 | const obj = {}; Object.getPrototypeOf(obj) | Object.prototype |
Object.setPrototypeOf(obj, proto) | 设置对象原型(不推荐频繁使用) | const obj = {}; const proto = { greet() { return 'hello' } }; Object.setPrototypeOf(obj, proto); obj.greet() | 原型设置成功 'hello' |
Object.assign
Object.assign() 用于将一个或多个源对象的可枚举属性复制到目标对象中,返回目标对象。常用于对象合并、浅拷贝、默认值合并等场景。
基本语法:
Object.assign(target, ...sources);
target:目标对象(被修改并返回)sources:一个或多个源对象
示例
- 合并对象
const a = { name: '小明' };
const b = { age: 20 };
const result = Object.assign({}, a, b);
console.log(result); // { name: '小明', age: 20 }
- 修改目标对象
const target = { a: 1 };
const source = { b: 2 };
Object.assign(target, source);
console.log(target); // { a: 1, b: 2 }(target 被改了)
- 浅拷贝
const obj1 = { nested: { x: 1 } };
const obj2 = Object.assign({}, obj1);
obj2.nested.x = 999;
console.log(obj1.nested.x); // 999 ❗️(浅拷贝,嵌套引用的是同一个对象)
- 会覆盖同名属性,右边覆盖左边
Object.assign({ a: 1 }, { a: 2 }); // { a: 2 }
- 会跳过不可枚举和原型属性
const obj = Object.create({ inherited: 123 });
obj.visible = 1;
Object.assign({}, obj); // 只复制 visible,不复制 inherited
浅拷贝和深拷贝
浅拷贝
const obj = {
a: 1,
b: {
c: 2
}
}
// 实现方法1
const resObj = { ...obj }
// 实现方法2
const resObj = Object.assign({}, obj)
// 实现方法3
function light_copy(obj) {
let resObj = {}
Object.keys(obj).forEach(t => resObj[t] = obj[t])
return resObj
}
深拷贝
const obj = {
a: 1,
b: {
c: 2
}
}
// 实现方法1
const resObj = JSON.parse(JSON.stringify(obj))
// 实现方法2
import _ from 'lodash';
const deepCopy = _.cloneDeep(obj);
// 实现方法3
function deep_copy1(obj) {
if (typeof obj !== 'object' || obj === null) return obj;
let resObj = Array.isArray(obj) ? [] : {}
for (const key in obj) {
resObj[key] = deep_copy1(obj[key])
}
return resObj
}
// 实现方法4 - 支持循环引用(坑)
const obj = { a: 1, b: 2 };
obj.c = obj; // c 指向 obj 自己
此时使用方法3,会死循环 💥!函数一直递归下去,因为:
- `obj.c = obj`,它指向自己。
- `deep_copy1(obj)` → `deep_copy1(obj.c)` → `deep_copy1(obj)` → ... 无限递归。
function deep_copy2(obj, cache = new WeakMap()) {
if (typeof obj !== 'object' || obj === null) return obj;
// 如果这个对象已经被拷贝过,直接返回这个对象
if (cache.has(obj)) return catch.get(obj);
let resObj = Array.isArray(obj) ? [] : {}
catch.set(obj, res) // 缓存
for (const key in obj) {
resObj[key] = deep_copy2(obj[key], cache)
}
return resObj
}
二十、虚拟列表
原理
只渲染用户当前能看到的一小部分数据,而不是渲染整个长列表,从而大大减少了DOM节点的数量,提升滚动性能和内存占用。
固定高度虚拟列表:
- 预先知道每一项的高度,比如每项50px;
- 根据容器高度计算出一屏能显示多少项:
可视数量 = 容器高度 / 单项高度; - 监听容器的
scroll事件,根据滚动距离计算出起始索引:起始索引 = Math.floor(滚动距离 / 单项高度); - 从起始索引开始截取一屏的数据进行渲染,同时上下追加几个作为缓冲区,避免快速滚动时白屏;
- 为了正确展示滚动条长度,使用div元素撑起总高度:
总高度 = 总数据量 * 单项高度; - 通过
transform: translateY()把真正渲染的内容推到正确的位置,顶部偏移量 = 起始索引 * 单项高度
<template>
<!-- 容器:负责滚动 -->
<div
class="virtual-list-container"
ref="containerRef"
@scroll="handleScroll"
:style="{ height: containerHeight + 'px' }"
>
<!-- 占位元素:撑起滚动条 -->
<div
class="virtual-list-phantom"
:style="{ height: totalHeight + 'px' }"
></div>
<!-- 实际渲染的内容区 -->
<div
class="virtual-list-content"
:style="{ transform: `translateY(${offsetY}px)` }"
>
<div
v-for="item in visibleData"
:key="item.id"
class="virtual-list-item"
:style="{ height: itemHeight + 'px' }"
>
{{ item.text }}
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const props = defineProps({
// 所有数据
data: { type: Array, default: () => [] },
// 每一项高度(固定)
itemHeight: { type: Number, default: 50 },
// 容器高度
containerHeight: { type: Number, default: 400 },
// 缓冲区数量(上下多渲染几项)
bufferCount: { type: Number, default: 5 }
})
const containerRef = ref(null)
const scrollTop = ref(0)
// 总高度
const totalHeight = computed(() => {
return props.data.length * props.itemHeight
})
// 一屏能显示多少项(加上缓冲区)
const visibleCount = computed(() => {
return Math.ceil(props.containerHeight / props.itemHeight) + props.bufferCount * 2
})
// 起始索引
const startIndex = computed(() => {
let index = Math.floor(scrollTop.value / props.itemHeight)
// 往前推缓冲区
index = Math.max(0, index - props.bufferCount)
return index
})
// 结束索引
const endIndex = computed(() => {
let index = startIndex.value + visibleCount.value
return Math.min(index, props.data.length)
})
// 当前可视区域的数据
const visibleData = computed(() => {
return props.data.slice(startIndex.value, endIndex.value)
})
// 顶部偏移量(把内容推到正确位置)
const offsetY = computed(() => {
return startIndex.value * props.itemHeight
})
// 滚动处理
const handleScroll = () => {
scrollTop.value = containerRef.value?.scrollTop || 0
}
</script>
<style scoped>
.virtual-list-container {
position: relative;
overflow-y: auto;
border: 1px solid #ddd;
}
.virtual-list-phantom {
position: absolute;
top: 0;
left: 0;
right: 0;
z-index: -1;
}
.virtual-list-content {
position: absolute;
top: 0;
left: 0;
right: 0;
}
.virtual-list-item {
display: flex;
align-items: center;
padding: 0 16px;
border-bottom: 1px solid #eee;
box-sizing: border-box;
}
</style>