JavaScript

162 阅读16分钟

一、JS的数据类型以及它们的区别

JS的数据类型分为:基础数据类型和引用数据类型。

  • 基础数据类型:Number、Boolean、String、Undefined、Null,以及ES6新增的Symbol、BigInt。
  • 引用数据类型:Object

其中,SymbolBigInt是ES6新增的数据类型:

  • Symbol:代表创建后独一无二且不可改变的数据类型,它主要是为了解决可能出现的全局变量冲突的问题。
  • BigInt:一种数字类型的数据。它可以表示任何精度格式的整数,使用BigInt可以安全存储和操作大整数,即使这个数已经超出了Number能够表示的安全整数范围。使用BigInt可以避免精度丢失。

二、数据类型检测的方式有哪些

  1. 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'
  1. 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只能判断引用数据类型,不能判断基本数据类型。

  1. 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就不能用来判断数据类型了

  1. 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使用情况

  1. 在普通函数中,this指向全局对象(在浏览器中指向window,在nodejs中指向global)
  2. 在函数作为对象的方法使用时,this指向该对象
  3. 箭头函数中的this,箭头函数没有自己的this,它的this永远指向它的作用域父级,且this指向不可变
  4. 构造函数中的this,指向实例对象
  5. callapplybind 方法,可以改变this指向

五、箭头函数与普通函数的区别

  1. 箭头函数更加简洁
const fn = () => {} //箭头函数
function fn() {} //普通函数
  1. 箭头函数没有自己的this,它的this继承于它的上级作用域
  2. 箭头函数继承来的this指向永远不会被改变
  3. call、apply、bind不会改变箭头函数的this指向
  4. 箭头函数不能作为构造函数使用
  5. 箭头函数没有自己的arguments
  6. 箭头函数没有prototype
  7. 箭头函数不能作为Generator生成器函数,不能使用yeild关键字

六、js的模块化

js的模块化概念:JS 模块化是为了解决作用域污染依赖混乱可维护性差的问题。随着项目变大,把代码拆成模块能让我们更好地管理依赖关系减少全局变量冲突,也方便团队协作。
模块化也为构建工具提供了基础,比如 tree-shaking按需加载等优化手段。

js的模块化分为:AMD、CommonJS、ES Module

AMD概念:AMD(Asynchronous Module Definition)是一种浏览器端的模块化规范,由 RequireJS 推广,强调异步加载模块。

CommonJS概念:CommonJS 是一种模块化规范,最初是为服务器端(Node.js)设计的,强调同步加载模块。

AMD和CommonJS的区别

  1. AMD是异步加载,适用于浏览器端
  2. CommonJS是同步加载,适用于nodejs

七、let、const、var区别

  1. 变量提升:var具有变量提升,let、const不存在变量提升
  2. 暂时性死区:let、const具有暂时性死区,var不存在暂时性死区
  3. 块级作用域:块级作用域是由{}包裹,let、const具有块级作用域,var没有块级作用域。
  4. 给全局添加属性:浏览器的全局对象是window,node的全局对象是global。var声明的变量为全局变量,并会将该变量添加到全局对象的属性上。但是let、const不会。
  5. 重复声明:var声明变量时,可以重复声明变量,后面声明的同名变量会覆盖前面的。let、const不允许重复声明。
  6. 初始值设置:const声明必须赋值,let、var可以不用设置初始值。
  7. 指针指向:var、let创建的变量可以改变指针指向(重新赋值),const声明的变量不可以改变指针指向。

八、块级作用域解决了ES5中的什么问题

  1. 解决了变量提升的问题:内层变量可以覆盖外层变量
    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)
    }
    
  2. 解决了变量覆盖问题
    var x = 10
    if (true) {
    var x = 20 // 重新赋值
    }
    console.log(x) // 20
    
  3. for循环变量共享的问题
    • var没有块级作用域,i 变成了全局变量。
    forvar i = 0; i < 3; i++){
    setTimeOut(function() {
    console.log(i)
    }, 0)
    }
    // 预期结果: 0,1,2
    // 实际结果: 3,3,3
    
    • let具有块级作用域,每次循环都会创建一个新的i,setTimeout中的i互不影响。
    forlet i = 0; i < 3; i++){
    setTimeOut(function() {
    console.log(i)
    }, 0)
    }
    // 实际结果: 0,1,2
    
  4. 变量重复声明的问题:var创建的变量可以重复声明,容易引发问题。let、const禁止重复声明变量。
    var x = 10
    var x = 20 // 重复声明不会报错
    console.log(x) // 20
    

九、new操作符的实现原理

  1. 新建一个空对象
  2. 将空对象的原型挂载到构造函数的原型上
  3. 将空对象的this指向构造函数
  4. 返回这个对象
// 实现
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是单线程的,也能执行异步任务不阻塞主任务。

  1. 首先会先执行同步任务,再执行异步任务,进入到事件循环过程中。
  2. 执行异步任务的时候又会区分微任务宏任务。先执行微任务
  3. 后执行宏任务,如果执行宏任务的过程中产生微任务的话,会把微任务放到微任务队列中。
  4. 执行完当前宏任务,立刻执行微任务,再执行下一轮宏任务
  • 同步任务:console.log()、变量声明、函数调用、for循环
  • 异步任务setTimeoutfetch、事件绑定(click)、Promise、DOM 渲染
  • 微任务
    1. Promise.then/catch/finallyAsync/Await
    2. MutationObserver监听 DOM 变动(浏览器原生)
    3. queueMicrotask(fn)手动添加微任务(浏览器和 Node 支持)
    4. process.nextTickNode.js 中的微任务,比 Promise 更早
  • 宏任务
    1. setTimeout(fn, 0)定时器
    2. setInterval(fn, 1000)定时重复任务
    3. setImmediate(fn)Node.js 特有,立即执行
    4. axios 网络请求

十三、闭包

  • 闭包:指在函数内部可以访问到函数外部的变量。
  • 在哪里使用过闭包:防抖、节流函数。

十四、Promise

Promise的概念:Promise是ES6新引入的异步编程解决方案。

Promise的状态

  1. pending 进行中
  2. fulfilled 已成功
  3. rejected 已失败

状态只能从 pending 变为 fulfilled或 rejected,一旦变更不可逆。

Promise的方法

  1. Promise.all: 可以放多个promise对象,会等所有promise对象都执行完成后一起返回。如果其中有一个promise对象执行失败,promise.all返回失败结果。
  2. Promise.race: race的意思是“赛跑”。它里面可以放多个promise对象,哪个promise对象先执行完成,先返回哪个promise对象。

十五、异步编程的实现方式

  1. Promise
  2. Async/Await
  3. 回调函数
  4. generator(生成器)函数

十六、内存泄露

  1. 意外的全局变量
  2. 被遗忘的计时器和回调函数
  3. 不正确的使用闭包
  4. 脱离DOM的引用

十七、ES6的新特性

  1. 箭头函数
  2. 新的变量声明方式let、const
  3. 解构赋值 const [a, b] = [1, 2]
  4. promise
  5. 模版字符串: 在代码层面可换行
const a = 'str'
console.log(`<p>${a}</p>
<div>${a}</div>`)
  1. 扩展运算符
const a = [1, 2, 3]
const b = [5, 6, 7]
const c = [...a, ...b] // [1, 2, 3, 5, 6, 7]
  1. Symbol数据类型,代表独一无二的值
  2. 模块化
  3. for...of 遍历对象的键值
  4. Map 和 Set复杂数据类型:
  5. Proxy代理
  6. 类(Class):面向对象编程的概念
  7. 默认参数(Default Parameter),在定义函数时可以给参数设置默认值

十八、数组

七个可以改变原数组的数组方法

unshift()、shift()、push()、pop()、sort()、reverse()、splice()

数组的其他常见方法

concat()、foreach()、map()、some()、every()

数组转换成字符串

  1. toString() 默认按逗号分隔 ["hello", "world"].toString()"hello,world"
  2. join() 将数组元素用指定分隔符连接成字符串 ["a", "b", "c"].join("-")"a-b-c"

数组foreach方法和map方法的区别

  1. forEach遍历数组执行副作用操作,没有返回值;map会返回一个新数组,常用于对数组进行映射转换;
  2. map可以支持链式调用,更适合函数式编程

数组foreach方法和map方法的相同点

都不会改变原数组

forEach方法可以主动中断吗

不可以,break/return/continue都不生效。

判断数组的方法有哪些

  1. 通过instanceof判断 [] instanceof Array
  2. Object.prototype.toString.call([])
  3. 通过ES6的Array.isArray([])判断
  4. 通过原型链判断 [].__proto__ === Array.prototype
  5. 通过Array.prototype.isPrototypeOf([])判断

十九、对象

对象的静态方法

Object 的静态方法主要用于对象的创建、拷贝、属性定义、遍历等,常见的有 assignkeysvaluesentriescreatedefinePropertyfreezeis 等等。

获取属性相关

对象方法示例表格

方法名作用示例输出结果
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:一个或多个源对象
示例
  1. 合并对象
const a = { name: '小明' };
const b = { age: 20 };
const result = Object.assign({}, a, b);
console.log(result); // { name: '小明', age: 20 }
  1. 修改目标对象
const target = { a: 1 };
const source = { b: 2 };
Object.assign(target, source);
console.log(target); // { a: 1, b: 2 }(target 被改了)
  1. 浅拷贝
const obj1 = { nested: { x: 1 } };
const obj2 = Object.assign({}, obj1);

obj2.nested.x = 999;
console.log(obj1.nested.x); // 999 ❗️(浅拷贝,嵌套引用的是同一个对象)
  1. 会覆盖同名属性,右边覆盖左边
Object.assign({ a: 1 }, { a: 2 }); // { a: 2 }
  1. 会跳过不可枚举和原型属性
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节点的数量,提升滚动性能和内存占用。

固定高度虚拟列表:

  1. 预先知道每一项的高度,比如每项50px;
  2. 根据容器高度计算出一屏能显示多少项:可视数量 = 容器高度 / 单项高度
  3. 监听容器的scroll事件,根据滚动距离计算出起始索引:起始索引 = Math.floor(滚动距离 / 单项高度);
  4. 从起始索引开始截取一屏的数据进行渲染,同时上下追加几个作为缓冲区,避免快速滚动时白屏;
  5. 为了正确展示滚动条长度,使用div元素撑起总高度:总高度 = 总数据量 * 单项高度;
  6. 通过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>