Interview -- JS 基础知识
在此梳理 JS 基础知识的一些问题。
目录
- 值类型和引用类型
- typeof 运算符
- 手写深拷贝
- 类型转换
- var 和 let const 的区别
- typeof 返回哪些类型
- 数组的 pop、push、unshift、shift 分别是什么?
- 数组有哪些纯函数方法
- 分片上传
- File 对象 和 Blob 对象的区别
- 深浅拷贝
- ESModule
值类型和引用类型
值类型(5 种): Number、String、Boolean、Symbol、undefined
引用类型(4 种):Object、Array、Null(特殊引用类型、指针指向空地址)、Function(特殊引用类型,但不存储数据,所以没有"拷贝、复制函数"的说法)
TS 中的数据类型,继承了 js 的类型,但是在这之上又做了一层扩展:
- 基本类型(小写 8 种):boolean、string、number、symbol、bigInt、undefined、 null、object
- 复杂类型由上面组合而成:
- js 中值类型的包装对象(除 Symbol 和 BingInt 不能作为构造函数使用):
Boolean()
、String()
、Number()
; - 其它复杂类型:
Array<T>
(数组类型一致)、Function
函数类型('Function'不推荐直接使用会报错)(...args: any[]) => void
- ts 中也有值类型:单个值,如
const x = 'ts'
x 的类型为 'ts'; - 扩展类型:any、unknown、never、void、enum 枚举、tuple 元组
[T]
(在括号内定义类型)
- js 中值类型的包装对象(除 Symbol 和 BingInt 不能作为构造函数使用):
typeof 运算符
作用: 识别所有值类型、识别函数和判断是否是引用类型(不可再细分)
手写深拷贝
步骤:
- 判断是否为 objec array 类型或者 null, 否则直接返回;
- 判断是否为数组类型, 若为数组则定义拷贝类型为数组类型,否则为对象类型;
- 遍历待拷贝数据的非原型属性, 则递归拷贝。(此处无需再判断属性是否为复杂类型,递归中会自行判断)
const obj1 = {
age: 20,
name: '小明',
address: {
city: '上海',
},
tool: ['a', 'b', 'c'],
}
function deepClone(obj) {
if (typeof obj === 'object' && obj !== null) {
const copyObj = obj instanceof Array ? [] : {} // 也可用 Array.isArray(obj),或者 Object.prototype.toString.call(obj) === '[object, Array]'
for (let key in obj) {
// 保证 key 不是原型属性
if (obj.hasOwnProperty(key)) {
// 递归调用
copyObj[key] = deepClone(obj[key])
}
}
return copyObj
}
return obj
}
类型转换
首先总结常用的隐式类型转换,能够产生隐式类型转换的有 if、逻辑运算、+-
拼接字符串和 ==
。
if 判断
if
其实判断的是truly
变量和falsely
变量。假设有一个变量为 a:
- 若
!!a === true
则表示为 truly 变量 - 若
!!a === false
则表示为 falsely 变量
falsely
变量有 6 个:0
、false
、''
(空字符串)、null
、undefined
和NaN
,其余全部是truly
变量。- 若
+-
字符串拼接const a = '100' + 10 // '10010' const b = '100' - 10 // 90 const c = true + '10' // 'true10' const d = true + 10 // '11' const e = true - '10' // '-9'
加号会转换为字符串拼接; 减号两边都会转换为数值, 再进行减法; Boolean 类型则看拼接的类型。
"=="
100 == '100' // true 0 == '' // true 0 == false // true false == '' // true null == undefined // true
除用
== null
判断null
或undefined
类型外, 其余一律用===
val == null
相当于val === null || val === undefined
,原因在于null
和undefined
在隐式转换时都会转换为false
。
总结规律:
- 如果两个操作数的类型相同,那么它们将按照相等性规则进行比较,返回相应的布尔值。
- 如果一个操作数是
null
,另一个操作数是undefined
,则它们相等。 - 如果一个操作数是数字,另一个操作数是字符串,JavaScript 会尝试将字符串转换为数字,然后进行比较。
- 如果一个操作数是布尔值,另一个操作数是非布尔值(除了
null
和undefined
),JavaScript 会尝试将布尔值转换为数字(true
转换为 1,false
转换为 0),然后进行比较。 - 如果一个操作数是对象,另一个操作数是原始类型(数字、字符串、布尔值),JavaScript 会尝试调用对象的
valueOf()
或toString()
方法,将对象转换为原始类型的值,然后进行比较。
强制类型转换(显式类型转换):
字符串转换:使用
String()
函数或toString()
方法将其他类型的值转换为字符串。let num = 123 let str = String(num) console.log(str) // 输出 "123" let bool = true let boolStr = bool.toString() console.log(boolStr) // 输出 "true"
数值转换:使用
Number()
函数将其他类型的值转换为数值,此外还有parseInt
和parseFloat
(parseInt
和parseFloat
的第二个参数为要转换成的进制)。let str = '456' let num = Number(str) console.log(num) // 输出 456 let bool = true let boolNum = Number(bool) console.log(boolNum) // 输出 1
布尔转换:使用
Boolean()
函数将其他类型的值转换为布尔值。let num = 0 let bool = Boolean(num) console.log(bool) // 输出 false let str = 'Hello' let strBool = Boolean(str) console.log(strBool) // 输出 true
var 和 let const 的区别
在 JavaScript 中,var
、let
和const
是用于声明变量的关键字,它们之间有一些重要的区别。
作用域:
var
声明的变量存在函数作用域或全局作用域,而let
和const
声明的变量存在块级作用域。块级作用域由一对大括号{}
来定义,例如 if 语句、循环或函数等。// 使用 var function example() { var x = 1 if (true) { var x = 2 // x 在整个函数作用域内都可见 console.log(x) // 输出 2 } console.log(x) // 输出 2 } // 使用 let function example() { let x = 1 if (true) { let x = 2 // x 只在块级作用域内有效 console.log(x) // 输出 2 } console.log(x) // 输出 1 }
变量提升(Hoisting):使用
var
声明的变量会被提升到其作用域的顶部,可以在声明之前访问到(但是值为undefined
),而使用let
和const
声明的变量不会被提升。// 使用 var console.log(x) // 输出 undefined var x = 1 // 使用 let console.log(x) // 抛出 ReferenceError: x is not defined let x = 1
重复声明:使用
var
可以多次声明同一个变量而不会报错,而使用let
或const
在同一个作用域内重复声明同一个变量会抛出错误。// 使用 var var x = 1 var x = 2 // 有效,但不推荐这样做 // 使用 let let x = 1 let x = 2 // 抛出 SyntaxError: Identifier 'x' has already been declared // 使用 const const x = 1 const x = 2 // 抛出 SyntaxError: Identifier 'x' has already been declared
可变性:使用
var
和let
声明的变量是可变的(可重新赋值),而使用const
声明的变量是不可变的(常量),一旦被赋值就不能再修改。// 使用 var var x = 1 x = 2 // 可以重新赋值 // 使用 let let x = 1 x = 2 // 可以重新赋值 // 使用 const const x = 1 x = 2 // 抛出 TypeError: Assignment to constant variable.
typeof 返回哪些类型
typeof
是 JavaScript 的一个操作符,用于返回一个值的数据类型。typeof
可以返回以下类型的字符串:
"undefined"
:表示未定义的值。"boolean"
:表示布尔值类型。"number"
:表示数字类型,包括整数和浮点数。"string"
:表示字符串类型。"object"
:表示对象或 null。注意,函数也是对象类型。"function"
:表示函数类型。"symbol"
:表示符号类型(ES2015 新增)。"bigint"
:表示大整数类型(ES2020 新增)。
以下是一些示例:
console.log(typeof undefined) // 输出 "undefined"
console.log(typeof true) // 输出 "boolean"
console.log(typeof 42) // 输出 "number"
console.log(typeof 'Hello') // 输出 "string"
console.log(typeof BigInt(123)) // 输出 "bigint"
console.log(typeof Symbol('foo')) // 输出 "symbol"
console.log(typeof {}) // 输出 "object"
console.log(typeof null) // 输出 "object",这是 typeof 的一个历史遗留问题
console.log(typeof function () {}) // 输出 "function"
需要注意的是,
typeof null
返回"object"
是 JavaScript 的一个历史遗留问题,实际上 null 是一个特殊的原始值,不属于对象类型。
数组的 pop、push、unshift、shift 分别是什么?
pop()
:pop()
方法从数组的末尾移除一个元素,并返回该元素的值。它会修改原始数组。const arr = [1, 2, 3, 4] const removedElement = arr.pop() console.log(arr) // 输出 [1, 2, 3] console.log(removedElement) // 输出 4
push()
:push()
方法向数组的末尾添加一个或多个元素,并返回新数组的长度。它会修改原始数组。const arr = [1, 2, 3] const newLength = arr.push(4, 5) console.log(arr) // 输出 [1, 2, 3, 4, 5] console.log(newLength) // 输出 5
unshift()
:unshift()
方法向数组的开头添加一个或多个元素,并返回新数组的长度。它会修改原始数组。const arr = [2, 3, 4] const newLength = arr.unshift(1) console.log(arr) // 输出 [1, 2, 3, 4] console.log(newLength) // 输出 4
shift()
:shift()
方法从数组的开头移除一个元素,并返回该元素的值。它会修改原始数组。const arr = [1, 2, 3, 4] const shiftedElement = arr.shift() console.log(arr) // 输出 [2, 3, 4] console.log(shiftedElement) // 输出 1
这些方法提供了对数组进行栈(后进先出)和队列(先进先出)操作的能力。pop()
和 push()
在数组的末尾进行操作,而 unshift()
和 shift()
在数组的开头进行操作。这些方法都会修改原始数组,并返回相应的值或长度。
数组有哪些纯函数方法?
纯函数是指在执行过程中不会修改原始数据,并且每次调用都返回一个新的数组。
concat()
: 连接两个或多个数组,并返回一个新数组,不会修改原始数组。const arr1 = [1, 2, 3] const arr2 = [4, 5, 6] const newArr = arr1.concat(arr2) console.log(newArr) // 输出 [1, 2, 3, 4, 5, 6]
slice()
: 返回一个新数组,其中包含原始数组中指定部分的浅拷贝,不会修改原始数组。const arr = [1, 2, 3, 4, 5] const newArr = arr.slice(1, 3) console.log(newArr) // 输出 [2, 3]
map()
: 创建一个新数组,其元素是对原始数组元素调用提供的函数的结果,不会修改原始数组。const arr = [1, 2, 3] const newArr = arr.map((item) => item * 2) console.log(newArr) // 输出 [2, 4, 6]
filter()
: 创建一个新数组,其中包含原始数组中满足条件的所有元素,不会修改原始数组。const arr = [1, 2, 3, 4, 5] const newArr = arr.filter((item) => item > 2) console.log(newArr) // 输出 [3, 4, 5]
除 pop、push、shift、unshift 外的几个非纯函数:
reduce()
: 对数组中的每个元素执行一个提供的函数,并将结果汇总为单个值,不会修改原始数组,没有返回数组不是纯函数。const arr = [1, 2, 3, 4, 5] const sum = arr.reduce((accumulator, item) => accumulator + item, 0) console.log(sum) // 输出 15
every()
: 检测数组中的所有元素是否都满足给定的条件,返回一个布尔值,不会修改原始数组,没有返回数组不是纯函数。const arr = [1, 2, 3, 4, 5] const allPositive = arr.every((item) => item > 0) console.log(allPositive) // 输出 true
some()
: 检测数组中是否至少有一个元素满足给定的条件,返回一个布尔值,不会修改原始数组,没有返回数组不是纯函数。const arr = [1, 2, 3, 4, 5] const hasNegative = arr.some((item) => item < 0) console.log(hasNegative) // 输出 false
forEach()
: 数组循环,不返回值,可修改原数组,不是纯函数。const arr = [1, 2, 3, 4] arr.forEach((item, index, array) => { array[index] = item * 2 }) console.log(arr) // 输出 [2, 4, 6, 8]
splice()
: splice() 方法可以用于删除、插入和替换数组中的元素。它接受多个参数,其中最常用的是起始索引和要删除的元素数量。非纯函数,会直接修改原始数组,并返回被删除的元素组成的新数组(如果有的话)。const arr = [1, 2, 3, 4, 5] const removed = arr.splice(1, 2) // 从索引 1 开始删除 2 个元素 console.log(arr) // 输出 [1, 4, 5] console.log(removed) // 输出 [2, 3]
分片上传
分片上传是指将一个大文件分割成多个较小的文件块,然后将每个文件块上传到服务器,最后将所有文件块合并成一个完整的文件。这样可以提高文件上传的效率和稳定性,并避免因网络中断或服务器故障导致文件上传失败的情况。
<input type="file" id="fileInput" />
<button onclick="uploadFile()">上传</button>
const fileInput = document.getElementById('fileInput')
const uploadFile = async () => {
const file = fileInput.files[0]
const chunkSize = 1024 * 1024 // 每个文件块的大小为1MB
const totalChunks = Math.ceil(file.size / chunkSize)
for (let i = 0; i < totalChunks; i++) {
const start = i * chunkSize
const end = Math.min(start + chunkSize, file.size)
const chunk = file.slice(start, end)
const formData = new FormData()
formData.append('file', chunk)
formData.append('chunk', i)
formData.append('totalChunks', totalChunks)
const response = await fetch('/upload', {
method: 'POST',
body: formData,
})
if (!response.ok) {
throw new Error('上传失败')
}
}
}
File 对象 和 Blob 对象的区别?
File 对象和 Blob 对象都表示一个二进制数据块, 它们都继承自 Blob 类。
主要区别在于 File 对象除了包含二进制数据外, 还包含了文件的元数据(如文件名和修改日期等)。
Blob 对象只包含二进制数据,不包含元数据。通常表示一个不与文件系统相关联的二进制数据块。
深浅拷贝
深拷贝
递归调用
JSON.parse(JSON.stringify(obj))
: 无法拷贝函数, 无法拷贝对象原型链上的属性, 无法处理循环引用, Symbol 也不行,无法拷贝undefined
。无法拷贝函数的原因在于: JSON 是一种数据交换格式,不支持函数的序列化。
JSON.stringify
会跳过对象中的函数属性,而不是将它们转换为 JSON 字符串。const obj = { a: 1, b: function () { console.log('Hello') }, } const jsonString = JSON.stringify(obj) console.log(jsonString) // 输出: {"a":1} const parsedObj = JSON.parse(jsonString) console.log(parsedObj.b) // 输出: undefined
无法拷贝
undefined
的原因在于:在 JSON 中,undefined
无法表示。JSON.stringify
会忽略对象中值为undefined
的属性,而数组中的undefined
会被转换为null
。const obj = { a: 1, b: undefined, } const jsonString = JSON.stringify(obj) console.log(jsonString) // 输出: {"a":1} const parsedObj = JSON.parse(jsonString) console.log(parsedObj.b) // 输出: undefined const arr = [1, undefined, 3] const jsonArrayString = JSON.stringify(arr) console.log(jsonArrayString) // 输出: [1,null,3] const parsedArray = JSON.parse(jsonArrayString) console.log(parsedArray) // 输出: [1, null, 3]
第三方插件
lodash.cloneDeep
浏览器环境, 用
window.structuredClone
, 但无法处理weakMap
这类。
浅拷贝
Object.assign()
: 一维深拷贝, 多维浅拷贝;- 扩展运算符: 对象一维深拷贝, 多维浅拷贝;
- 数组方法实现浅拷贝: slice(), concat();
ESModule
CommonJS 和 ESModule 的主要区别是:
- CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用。
- CommonJS 模块是运行时加载,ES6 模块是编译时输出接口。
- CommonJS 模块的
require()
是同步加载模块,ES6 模块的import
命令是异步加载,有一个独立的模块依赖的解析阶段。
CommonJS 语法
动态导入, 运行时确定模块路径, 模块缓存, 循环依赖。
1.导出:
var counter = 3
function incCounter() {
counter++
}
module.exports = {
counter: counter,
incCounter: incCounter,
}
2.导入:
const myModule = require('./myModule.js')
ESModule 语法
ES6 模块是编译时加载,使得静态分析成为可能。
1.导出:
// 写法一
export const m = 1
// 写法二
const m = 1
export { m }
// 写法三
const n = 1
export { n as m }
export 语句输出的接口,与其对应的值是动态绑定关系,即通过该接口,可以取到模块内部实时的值。
export var foo = 'bar'
setTimeout(() => (foo = 'baz'), 500)
// 代码输出变量foo,值为bar,500 毫秒之后变成baz。
默认导出:
// export-default.js
export default function foo() {
console.log('foo');
}
// 或者写成
function foo() {
console.log('foo');
}
export default foo;
因为 export default 命令其实只是输出一个叫做 default 的变量,所以它后面不能跟变量声明语句。
// modules.js
function add(x, y) {
return x * y
}
export { add as default }
// 等同于
// export default add;
// app.js
import { default as foo } from 'modules'
// 等同于
// import foo from 'modules';
2.导入:
// 写法一
import { firstName, lastName, year } from './profile.js'
// 写法二 重命名
import { lastName as surname } from './profile.js'
- import 命令输入的变量都是只读的,因为它的本质是输入接口。
- import 命令具有提升效果,会提升到整个模块的头部,首先执行。
- import 命令只会导入同样的包一次, 不会重复执行。
3.动态导入:
ES2020 提案 引入 import()
函数,支持动态加载模块。
场景: 按需加载和条件加载, 还有一个是动态的模块路径:
import(f())
.then(...);
4.i
i
属性可以拿到当前模块的元信息。但只能在模块内部使用,如果在模块外部使用会报错。
i
:mport.meta.url i
返回当前模块的 URL 路径。举例来说,当前模块主文件的路径是mport.meta.url https://foo.com/main.js
,i
就返回这个路径。如果模块里面还有一个数据文件 data.txt,那么就可以用下面的代码,获取这个数据文件的路径。mport.meta.url new URL('data.txt', import.meta.url)
i
:mport.meta.scriptElement i
是浏览器特有的元属性,返回加载模块的那个mport.meta.scriptElement <script>
元素,相当于document.currentScript
属性。// HTML 代码为 // <script type="module" src="my-module.js" data-foo="abc"></script> // my-module.js 内部执行下面的代码 import.meta.scriptElement.dataset.foo // "abc"
循环加载
CommonJS 和 ES6 模块的区别:
CommonJS 中
require
命令第一次加载该脚本,就会执行整个脚本,然后在内存生成一个对象。只会在第一次加载时运行一次,以后再加载,就返回第一次运行的结果。{ id: '...', exports: { ... }, // 导出内容 loaded: true, ... }
结论: CommonJS 输入的是被输出值的拷贝,不是引用。当发生相互引用时, 由于 CommonJS 是运行时加载, 会随时停下来, 相互引用中, 只会读取到已经执行的部分。
ESModule 中是动态引用,如果使用 import 从一个模块加载变量(即
import foo from 'foo'
),那些变量不会被缓存,而是成为一个指向被加载模块的引用,需要开发者自己保证,真正取值的时候能够取到值。// a.mjs import { bar } from './b' console.log('a.mjs') console.log(bar) export let foo = 'foo' // b.mjs import { foo } from './a' console.log('b.mjs') console.log(foo) export let bar = 'bar'
上面例子中会报错, 原因在于 a 中引入了 b, 然后 b 中又引入了 a, 这时 a 还没有执行完, 导致 b 中取不到 a 的值。正确做法是将 b 中所需要使用的 a 模块的变量, 写成函数, 因为函数具有提升作用, 会在 import 之前先定义, 此时 b 中可以访问 a 模块中的值。