Interview -- JS 基础知识

Huy大约 15 分钟anonymousnote

在此梳理 JS 基础知识的一些问题。

目录

值类型和引用类型

值类型(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](在括号内定义类型)

typeof 运算符

作用: 识别所有值类型、识别函数和判断是否是引用类型(不可再细分)

手写深拷贝

步骤:

  1. 判断是否为 objec array 类型或者 null, 否则直接返回;
  2. 判断是否为数组类型, 若为数组则定义拷贝类型为数组类型,否则为对象类型;
  3. 遍历待拷贝数据的非原型属性, 则递归拷贝。(此处无需再判断属性是否为复杂类型,递归中会自行判断)
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 个:0false''(空字符串)、nullundefinedNaN其余全部是 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 判断 nullundefined 类型外, 其余一律用 ===

    val == null 相当于 val === null || val === undefined,原因在于 nullundefined 在隐式转换时都会转换为 false

总结规律:

  1. 如果两个操作数的类型相同,那么它们将按照相等性规则进行比较,返回相应的布尔值。
  2. 如果一个操作数是 null,另一个操作数是 undefined,则它们相等。
  3. 如果一个操作数是数字,另一个操作数是字符串,JavaScript 会尝试将字符串转换为数字,然后进行比较。
  4. 如果一个操作数是布尔值,另一个操作数是非布尔值(除了 nullundefined),JavaScript 会尝试将布尔值转换为数字(true 转换为 1,false 转换为 0),然后进行比较。
  5. 如果一个操作数是对象,另一个操作数是原始类型(数字、字符串、布尔值),JavaScript 会尝试调用对象的 valueOf()toString() 方法,将对象转换为原始类型的值,然后进行比较。

强制类型转换(显式类型转换):

  1. 字符串转换:使用 String() 函数或 toString() 方法将其他类型的值转换为字符串。

    let num = 123
    let str = String(num)
    console.log(str) // 输出 "123"
    
    let bool = true
    let boolStr = bool.toString()
    console.log(boolStr) // 输出 "true"
    
  2. 数值转换:使用 Number() 函数将其他类型的值转换为数值,此外还有 parseIntparseFloatparseIntparseFloat 的第二个参数为要转换成的进制)。

    let str = '456'
    let num = Number(str)
    console.log(num) // 输出 456
    
    let bool = true
    let boolNum = Number(bool)
    console.log(boolNum) // 输出 1
    
  3. 布尔转换:使用 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 中,varletconst是用于声明变量的关键字,它们之间有一些重要的区别。

  1. 作用域:var声明的变量存在函数作用域或全局作用域,而letconst声明的变量存在块级作用域。块级作用域由一对大括号 {} 来定义,例如 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
    }
    
  2. 变量提升(Hoisting):使用 var 声明的变量会被提升到其作用域的顶部,可以在声明之前访问到(但是值为 undefined),而使用 letconst 声明的变量不会被提升。

    // 使用 var
    console.log(x) // 输出 undefined
    var x = 1
    
    // 使用 let
    console.log(x) // 抛出 ReferenceError: x is not defined
    let x = 1
    
  3. 重复声明:使用 var 可以多次声明同一个变量而不会报错,而使用 letconst 在同一个作用域内重复声明同一个变量会抛出错误。

    // 使用 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
    
  4. 可变性:使用 varlet 声明的变量是可变的(可重新赋值),而使用 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 可以返回以下类型的字符串:

  1. "undefined":表示未定义的值。
  2. "boolean":表示布尔值类型。
  3. "number":表示数字类型,包括整数和浮点数。
  4. "string":表示字符串类型。
  5. "object":表示对象或 null。注意,函数也是对象类型。
  6. "function":表示函数类型。
  7. "symbol":表示符号类型(ES2015 新增)。
  8. "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 分别是什么?

  1. pop(): pop() 方法从数组的末尾移除一个元素,并返回该元素的值。它会修改原始数组

    const arr = [1, 2, 3, 4]
    const removedElement = arr.pop()
    console.log(arr) // 输出 [1, 2, 3]
    console.log(removedElement) // 输出 4
    
  2. 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
    
  3. unshift(): unshift() 方法向数组的开头添加一个或多个元素,并返回新数组的长度。它会修改原始数组

    const arr = [2, 3, 4]
    const newLength = arr.unshift(1)
    console.log(arr) // 输出 [1, 2, 3, 4]
    console.log(newLength) // 输出 4
    
  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() 在数组的开头进行操作。这些方法都会修改原始数组,并返回相应的值或长度。

数组有哪些纯函数方法?

纯函数是指在执行过程中不会修改原始数据,并且每次调用都返回一个新的数组。

  1. 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]
    
  2. slice(): 返回一个新数组,其中包含原始数组中指定部分的浅拷贝,不会修改原始数组。

    const arr = [1, 2, 3, 4, 5]
    const newArr = arr.slice(1, 3)
    console.log(newArr) // 输出 [2, 3]
    
  3. map(): 创建一个新数组,其元素是对原始数组元素调用提供的函数的结果,不会修改原始数组。

    const arr = [1, 2, 3]
    const newArr = arr.map((item) => item * 2)
    console.log(newArr) // 输出 [2, 4, 6]
    
  4. 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 外的几个非纯函数:

  1. reduce(): 对数组中的每个元素执行一个提供的函数,并将结果汇总为单个值,不会修改原始数组,没有返回数组不是纯函数。

    const arr = [1, 2, 3, 4, 5]
    const sum = arr.reduce((accumulator, item) => accumulator + item, 0)
    console.log(sum) // 输出 15
    
  2. every(): 检测数组中的所有元素是否都满足给定的条件,返回一个布尔值,不会修改原始数组,没有返回数组不是纯函数。

    const arr = [1, 2, 3, 4, 5]
    const allPositive = arr.every((item) => item > 0)
    console.log(allPositive) // 输出 true
    
  3. some(): 检测数组中是否至少有一个元素满足给定的条件,返回一个布尔值,不会修改原始数组,没有返回数组不是纯函数。

    const arr = [1, 2, 3, 4, 5]
    const hasNegative = arr.some((item) => item < 0)
    console.log(hasNegative) // 输出 false
    
  4. forEach(): 数组循环,不返回值,可修改原数组,不是纯函数。

    const arr = [1, 2, 3, 4]
    arr.forEach((item, index, array) => {
      array[index] = item * 2
    })
    console.log(arr) // 输出 [2, 4, 6, 8]
    
  5. 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 对象只包含二进制数据,不包含元数据。通常表示一个不与文件系统相关联的二进制数据块。

深浅拷贝

深拷贝

  1. 递归调用

  2. 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]
      
  3. 第三方插件 lodash.cloneDeep

  4. 浏览器环境, 用 window.structuredClone, 但无法处理 weakMap 这类。

浅拷贝

  1. Object.assign() : 一维深拷贝, 多维浅拷贝;
  2. 扩展运算符: 对象一维深拷贝, 多维浅拷贝;
  3. 数组方法实现浅拷贝: 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.import.meta:

import.meta 属性可以拿到当前模块的元信息。但只能在模块内部使用,如果在模块外部使用会报错。

  • import.meta.url: import.meta.url返回当前模块的 URL 路径。举例来说,当前模块主文件的路径是 https://foo.com/main.jsimport.meta.url就返回这个路径。如果模块里面还有一个数据文件 data.txt,那么就可以用下面的代码,获取这个数据文件的路径。

    new URL('data.txt', import.meta.url)
    
  • import.meta.scriptElement: import.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 模块的区别:

  1. CommonJS 中 require 命令第一次加载该脚本,就会执行整个脚本,然后在内存生成一个对象。只会在第一次加载时运行一次,以后再加载,就返回第一次运行的结果。

    {
      id: '...',
      exports: { ... }, // 导出内容
      loaded: true,
      ...
    }
    

    结论: CommonJS 输入的是被输出值的拷贝,不是引用。当发生相互引用时, 由于 CommonJS 是运行时加载, 会随时停下来, 相互引用中, 只会读取到已经执行的部分。

  2. 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 模块中的值。

Loading...