JavaScript 常见问答

Javascript 有哪些标准内置对象?

Javascript 标准内置对象也称全局的对象,这个全局的对象不应与全局对象(global 对象)混淆。

全局的对象 指的是处在全局作用域里的对象,对象可由用户的脚本创建,或由宿主程序提供。比如:Math 对象、String 对象、Number 对象等。

全局对象 指的是 Javascript 中一个特殊的对象,它及它的所有属性都可以在程序的任何地方访问,也称全局变量,一般用关键字 this 引用全局对象。ECMAScript 标准没有规定全局对象的类型,全局对象的定义取决于宿主环境。比如:浏览器的全局对象是 Window 对象,浏览器内所有的全局属性和函数都是 window 对象的属性和方法;node 的全局对象是 global 对象。

Javascript 标准内置对象的分类:

// 值属性
Infinity
NaN
undefined
globalThis

// 函数属性
eval()
uneval()
isFinite()
isNaN()
parseFloat()
parseInt()
decodeURI()
decodeURIComponent()
encodeURI()
encodeURIComponent()

// 基本对象
Object
Function
Number
String
Boolean
RegExp
Date
Math
Symbol
BigInt

// 集合对象
MapSetWeakMapWeakSet
ArrayArrayBufferSharedArrayBufferInt8ArrayUint8ArrayUint8ClampedArrayInt16ArrayUint16ArrayInt32ArrayUint32ArrayFloat32ArrayFloat64ArrayBigInt64ArrayBigUint64Array

其他对象:

Error
AggregateErrorEvalErrorInternalErrorRangeErrorReferenceErrorSyntaxErrorTypeErrorURIError

Atomics
DataView
JSON

Promise
Generator
GeneratorFunction
AsyncFunction

Reflect
Proxy

arguments

国际化: ECMAScript 核心的附加功能,用于支持多语言处理。

Intl
Intl.CollatorIntl.DateTimeFormatIntl.ListFormatIntl.NumberFormatIntl.PluralRulesIntl.RelativeTimeFormatIntl.Locale

WebAssembly:

WebAssembly
WebAssembly.ModuleWebAssembly.InstanceWebAssembly.MemoryWebAssembly.TableWebAssembly.CompileErrorWebAssembly.LinkError (en-US)、WebAssembly.RuntimeError

如何编写高性能的 JavaScript?

高性能首先来自少阻塞主线程、少触发布局、少做重复工作。

换成位运算、缓存 length 这类微优化,只有剖析证明它是热点之后才值得做。

加载和执行

deferasync 的经典 <script> 会打断 HTML 解析,直到脚本下载、编译、执行完。

常见做法:

  • 把阻塞脚本放到 </body> 前,或给脚本加 defer(保序,在 DOMContentLoaded 前执行)
  • async 下载完就执行,不保序,适合独立统计、广告类脚本
  • type="module" 默认按 defer 处理,并用 import() 按需加载
  • 动态插入 <script>:下载和执行可以错开,但仍占主线程;大模块优先用动态 import()

合并多个文件、减少 <script> 个数,在 HTTP/1.1 下能少连接。HTTP/2、HTTP/3 下请求合并的收益下降,仍要控制体积和解析成本(拆包、tree-shaking),不要为「少几个标签」把首屏用不到的代码打进同一个包。

运行时

  • 过深的嵌套循环:先看算法复杂度,热点再用循环展开或换数据结构。
  • 过长的原型链:引擎会缓存查找,但热点里反复走超长链仍有成本;更常见的问题是抽象层叠太多。
  • evalnew Function:无法按普通函数那样预编译优化,还有注入风险,不要当常规手段。
  • 未声明就赋值:在非严格模式会挂到全局对象上,既慢也容易泄漏;用模块或 "use strict"
  • 位运算会把操作数转成 32 位整数,不是四则运算的通用加速器;只在确实要做掩码、移位时用。
  • 简单 for 里读 arr.length,现行引擎通常已优化。跨函数、跨作用域反复读同一属性,仍可用局部变量缓存。
  • 不要用 for-in 遍历数组:它走可枚举属性(含原型链),顺序也不适合当索引循环。数组用 forfor-offorEach

DOM 与内存

DOM 操作通常比纯计算贵。减少访问次数、用局部变量存反复用的节点、事件委托、读写分离,避免「读布局 → 写样式 → 再读布局」造成的布局抖动。能离线拼的结构用 DocumentFragment 或先改后再一次性插回。

现代 GC 收得了普通对象之间的循环引用。真正拖住内存的,常见是:脱离文档仍被引用的 DOM、闭包抓住大对象、全局缓存、以及没卸掉的监听器和定时器。

注意:先用 Performance 面板或 performance.mark 找到瓶颈,再改那一处。没有测量的「优化」很容易改错方向。

你所不知道的“foo”和“bar”?

foobarbaz 是占位标识符(metasyntactic variable):示例里当变量名、函数名用,本身没有业务含义。示例输出常用 hello world,示例名字则常用这一串。

需要多个名字时,习惯顺序是 foobarbaz,再往后是 quxquux 等(《新黑客辞典》里的标准清单)。foobarfoobar 拼在一起,也常单独当一个示例名。

foo 从哪来

目前能追溯到的较早书面用法,是 Bill Holman 的消防队员漫画《Smokey Stover》(约 1930-1952)。画里车牌和无意义对白大量出现 FOO,例如 “He who foos last foos best”、“Many smoke but foo men chew”。Holman 自称这个词来自旧金山唐人街一座玉石小雕像底座上的字;后人常把它和汉语「福」的音译(fufoo)联系起来,这是作者说法加民间对应,不是严密考据。英文能接纳这个造词,也沾了意第绪语 feh、英语 fooeyfool 的边。

二战期间英美士兵俚语把 foo 带得更广。雷达上的不明回波叫 foo fighter(foo 战机),后来常被拿来对应 UFO。英军驻地也出现过 “Foo was here.” 一类涂鸦。

foobar 从哪来

bar 连用时,一般追溯到美军俚语 FUBAR:Fucked Up Beyond All Repair(一团糟,没法修),后来写成 foobar。《新黑客辞典》早期认为这是战后的雅化;后来的说法反过来:FUBAR 从已有的 foo 衍生,并受到德语 furchtbar(可怕)影响,有资料主张 foobar 才是更早的形式。词源并不只有一条线。

技术文档里,foobar 多半在 1960 年代末到 1970 年代初,经 Digital Equipment Corporation(DEC,迪吉多)的系统手册传开,有记录的用例至少到 1972 年。另一条工程侧解释:电子学里低电平有效的信号常在名字上方画横线,横线英语叫 bar,「反相的 foo 信号」读成 foo bar,也便于这个词在工程师口头里站住。

RFC 3092(2001 年 4 月 1 日的《Etymology of "Foo"》)专门考过这些词:截至当时,约 212 份 RFC、大约 7% 的 RFC 把 foobarfoobar 当占位符用,却没有给出定义。

注意:这是文档和示例里的习惯名,不是语言关键字。正式代码、对外 API 不要用 foobar 当真实业务名。

如何生成随机数?

Math.random() 函数返回一个浮点数,伪随机数在范围从 0 到小于1,也就是说,从 0(包括 0)往上,但是不包括 1,然后缩放到所需的范围。

「真正的真随机」目前只能用量子力学获取。一般的所谓真随机是指统计意义上的随机,也就是具备不确定性,可以被安全的用于金融等领域——计算机系统可以产生统计意义上的真随机数。大部分程序和语言中的随机数,确实都只是伪随机,即由可确定的函数(常用线性同余),通过一个种子(常用时钟)产生的。这意味着:如果知道了种子,或者已经产生的随机数,都可能获得接下来随机数序列的信息(可预测性)。

计算机系统本身是确定的、可预测的:一行行的代码是固定的,一步步的算法是固定的,一个个与非门是固定的,仅通过这些固定的东西是不可能产生真随机。但是,我们可以引入一些系统以外的变量,比如:硬件,一个典型的例子是 Unix 内核中的随机数发生器,理论上它能产生真随机,即这个随机数的生成,独立于生成函数,这时我们说这个随机数发生器是非确定的。

具体来讲,Unix 维护了一个熵池,不断收集非确定性的设备事件,即机器运行环境中产生的硬件噪音来作为种子,比如,时钟、IO 请求的响应时间、特定硬件中断的时间间隔、键盘敲击速度、鼠标位置变化、周围的电磁波等等……生成随机数。

// 生成 [0, 1) 范围内的随机数
Math.random()

// 生成 [n, m) 范围内的随机数
n + Math.random() * (m - n)

// 生成 [n, m] 范围内的随机整数
function getRandomIntInclusive(min, max) {
    min = Math.ceil(min)
    max = Math.floor(max)
    return Math.floor(Math.random() * (max - min + 1)) + min
}

// 生成指定位数的只有纯数字的字符串
function randomNum(n){
    var res = "";
    for(let i = 0; i < n; i ++){
        res += Math.floor(Math.random() * 10)
    }
    return res
}

// 生成指定位数的有数字、字母的字符串
function randomNumOrLetter(n) {
    let chars = [
        '0','1','2','3','4','5','6','7','8','9',
        'A','B','C','D','E','F','G','H','I','J','K','L','M',
        'N','O','P','Q','R','S','T','U','V','W','X','Y','Z'
    ]
    let res = ''
    for (let i = 0; i < n; i ++) {
        let id = Math.floor(Math.random()*36)
        res += chars[id];
    }
    return res
}

注意: Math.random() 不能提供像密码一样安全的随机数字,所以不要使用它们来处理有关安全的事情。可使用更精确的 window.crypto.getRandomValues() 来代替。

如何实现数组的随机排序?

方法一: 使用数组 sort 方法对数组元素随机排序,让 Math.random() 随机数与 0.5 比较,如果大于就返回 1 交换位置,如果小于就返回 -1,不交换位置。

注意: 每个元素被派到新数组的位置不是随机的,因为 sort() 方法是依次比较的。

function randomSort(arr) {
    return arr.sort((a, b) => {
        return Math.random() > 0.5 ? -1 : 1;
    })
}

方法二: 随机从原数组抽取一个元素,加入到新数组。

function randomSort(arr) {
    let result = []
    while (arr.length > 0) {
        let index = Math.floor(Math.random() * arr.length)
        result.push(arr[index])
        arr.splice(index, 1)
    }
    return result
}

方法三: 随机交换数组内的元素。

function randomSort(arr) {
    for (let index = 0; index < arr.length; index++) {
        const rIndex = Math.floor(Math.random() * (len - index)) + index

        const temp = arr[index]
        arr[index] = arr[rIndex]
        arr[rIndex] = temp
    }
    return arr
}

创建对象的几种方式?

在 JavaScript 可以使用 Object 构造函数或对象字面量来创建单个对象,但是这些方法都有一个明显的缺点:使用同一个接口创建很多对象,会产生大量的重复代码

为了解决这个问题,人们提出了很多对象创建的解决办法:

工厂模式

工厂模式是软件工程领域一种广为人知的设计模式,这种模式抽离了创建对象的具体过程。

在 ES6 之前,JavaScript 无有类的概念,开发人员用函数封装特定接口来创建对象的细节。

function createPerson (name, age, job){
    const o = new Object()
    o.name = name
    o.age = age
    o.job = job
    o.sayName = function() {
        console.log(this.name)
    }
    return o
}
const person1 = createPerson("james", 9, "student")
const person2 = createPerson("kobe", 9, "student")

缺点:创建的对象没有和类型建立关系,即不知道一个对象的类型是什么。

构造函数模式

JavaScript 中,只要一个函数是通过 new 来调用的,那么就可以把它称为构造函数。new 操作符用于创建一个对象类型或者构造函数的实例。

new 操作符进行如下的操作:

  • 创建一个空的新对象(即 {});
  • 为新对象添加属性 proto,将该属性指向构造函数的 prototype 属性;
  • 将新对象作为 this 的上下文 ;
  • 如果该函数没有返回对象,则返回 this。
function createPerson(name, age, job){
    this.name = name
    this.age = age
    this.job = job
    this.sayName = function(){
        console.log(this.name)
    }
}
const person1 = new createPerson("james", 9, "student")
const person2 = new createPerson("kobe", 9, "student")

优点:所创建的对象和构造函数建立起了联系

缺点:造成了不必要的函数对象的创建,而函数是所有的实例都可以通用的。

原型模式

每一个函数都有一个 prototype 属性,这个属性是一个对象,它包含了通过构造函数创建的所有实例都能共享的属性和方法。因此可以使用原型对象来添加公用属性和方法,从而实现代码的复用。

function Person(){}
Person.prototype.name = "james"
Person.prototype.age = 9
Person.prototype.job = "student"
Person.prototype.sayName = function(){
    console.log(this.name)
}
const person1 = new Person()
const person2 = new Person()
person1.sayName()
person2.sayName()

console.log(person1.sayName === person2.sayName)

优点:解决了函数对象的复用问题

缺点:一个是没有办法通过传入参数来初始化值,另一个是如果存在一个引用类型,如 Array,那么所有的实例将共享一个对象,一个实例对引用类型值的改变会影响所有的实例。

构造函数 + 原型模式(推荐)

通过构造函数来初始化对象的属性,通过原型对象来实现函数方法的复用。

function Person(name, age, job) {
    this.name = name
    this.age = age
    this.job = job
}
Person.prototype = {
    sayName: function () {
        alert(this.name)
    }
}
const person1 = new Person("james", 9, "student")
const person2 = new Person("kobe", 9, "student")

console.log(person1.name)
console.log(person2.name)
console.log(person1.sayName === person2.sayName)

这种方法很好的解决了两种模式单独使用时的缺点,但是有一点不足的是对于代码的封装性不够好

几种继承的实现方式?

继承是面向对象语言中最重要的一个概念。许多面向对象语言都支持两种继承方式:接口继承和实现继承。接口继承只继承方法和签名,而实现继承则继承实际的方法。由于在 JavaScript 中函数没有签名,因此无法实现接口继承,只支持实现继承

原型链继承

其基本思想是利用原型让一个引用类型继承另一个引用类型的属性和方法。

function Person(props = {}) {
    this.name = props.name || 'Unnamed'
}
Person.prototype.hello = function () {
    console.log('Hello, ' + this.name + '!')
}

function Student(props = {}) {
    this.grade = props.grade || 1
}
Student.prototype = new Person()
Student.prototype.constructor = Student
const student1 = new Student({ name: 'lizh' })

优点:父类的方法可以复用,子类实例不需要单独创建方法。

缺点:父类的引用属性会被所有子类实例共享;子类构建实例时不能向父类传递参数。

构造函数继承

其主要思路是在子类型的构造函数中调用超类型构造函数。

function SuperType() {
    this.colors = ["red", "blue", "green"]
}

function SubType() {
    SuperType.call(this)
}
const instance1 = new SubType()
instance1.colors.push("black")
console.log(instance1.colors) // ['red', 'blue', 'green', 'black']

const instance2 = new SubType()
console.log(instance2.colors) // ['red', 'blue', 'green']

优点:父类的引用属性不会被共享,子类构建实例时可以向父类传递参数。

缺点:父类的方法不能复用,子类实例的方法每次都是单独创建的。

原型链 + 构造函数继承(推荐)

这种方法的主要思路是使用原型链实现对原型属性和方法的继承,而通过构造函数来实现对实例属性的继承。

这样既通过在原型上定义方法实现了函数复用,又能够保证每个实例都有它自己的属性。

function SuperType(name) {
    this.name = name
    this.colors = ["red", "blue", "green"]
}
SuperType.prototype.sayName = function () {
    console.log(this.name)
}

function SubType(name, age) {
    SuperType.call(this, name)
    this.age = age
}
SubType.prototype = new SuperType()
SubType.prototype.constructor = SubType
SubType.prototype.sayAge = function () {
    console.log(this.age)
}
const instance1 = new SubType("james", 9)
instance1.colors.push("black")
console.log(instance1.colors) // ['red', 'blue', 'green', 'black']
instance1.sayName() // james
instance1.sayAge() // 9

const instance2 = new SubType("kobe", 10)
console.log(instance2.colors) // ['red', 'blue', 'green']
instance2.sayName() // kobe
instance2.sayAge() // 10

优点:父类的方法可以被复用,父类的引用属性不会被共享,子类构建实例时可以向父类传递参数,这是 JavaScript 中最常用的继承模式。而且,instanceofisPropertyOf() 也能够用于识别基于组合继承创建的对象。

缺点:调用了两次父类的构造函数,第一次在子类调用父类构造函数,第二次是在给子类设置 prototype 属性。造成了子类型的 prototype 中多了很多不必要的属性

原型式继承

主要思路是传入一个对象,返回一个原型对象为该对象的新对象。

function newObject(o) {
    function F() { }
    F.prototype = o
    return new F()
}
const person = {
    name: "Nicholas",
    friends: ["shelby", "Count", "Van"]
}
const person1 = newObject(person)
person1.name = "Gerg"
person1.friends.push("Job")
console.log(person1.name, person1.friends)

const person2 = newObject(person)
console.log(person2.name, person2.friends)

ECMAScript 5 中新增了 Object.create() 方法规范了原型式继承。这个方法接收两个参数,一个是将被用作新对象原型的对象,一个是为新对象定义额外属性的对象(可选)。

const person = {
    name: "Nicholas",
    friends: ["shelby", "Count", "Van"]
}
const person1 = Object.create(person)
person1.name = "Gerg"
person1.friends.push("Job")
console.log(person1.name, person1.friends)

const person2 = Object.create(person)
console.log(person2.name, person2.friends)

优点:父类方法可以复用。

缺点:父类的引用属性会被所有子类实例共享,子类构建实例时不能向父类传递参数。

© lizhao all right reserved,powered by Gitbook文件修订时间: 2026-08-26 20:30:50

results matching ""

    No results matching ""