常见问答

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

// 集合对象
Map、Set、WeakMap、WeakSet
Array、ArrayBuffer、SharedArrayBuffer、Int8Array、Uint8Array、Uint8ClampedArray、Int16Array、Uint16Array、Int32Array、Uint32Array、Float32Array、Float64Array、BigInt64Array、BigUint64Array

其他对象:

Error
AggregateError、EvalError、InternalError、RangeError、ReferenceError、SyntaxError、TypeError、URIError

Atomics
DataView
JSON

Promise
Generator
GeneratorFunction
AsyncFunction

Reflect
Proxy

arguments

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

Intl
Intl.Collator、Intl.DateTimeFormat、Intl.ListFormat、Intl.NumberFormat、Intl.PluralRules、Intl.RelativeTimeFormat、Intl.Locale

WebAssembly:

WebAssembly
WebAssembly.Module、WebAssembly.Instance、WebAssembly.Memory、WebAssembly.Table、WebAssembly.CompileError、WebAssembly.LinkError (en-US)、WebAssembly.RuntimeError

什么是 IIFE 和 闭包?

IIFE

IIFE(Immediately Invoked Function Expression,立即调用函数表达式),是一个在定义时就会立即执行的 JavaScript 函数。

const func = (function () { 
  const name = "Lizhao"; 
  return name; 
})(); 
func; // "Lizhao"

IIFE 有以下特性:

  • 当函数变成立即执行的函数表达式时,表达式中的变量不能从外部访问。

  • 将 IIFE 分配给一个变量,不是存储 IIFE 本身,而是存储 IIFE 执行后返回的结果。

闭包(closure)

闭包(closure),是一个函数里面嵌套另外一个函数,被嵌套(内部)的函数自身形成了一个闭包,这个闭包可以自己拥有独立的词法环境,也就意味着,被嵌套函数的作用域除了自身的作用域,还维持了其容器函数的作用域。

一句话概括: 闭包就是一个拥有自身作用域和父级作用域(由外层函数提供,即便外层函数销毁依旧可以访问)的特殊函数。

const func = function () {
  const name = "Lizhao";
  return function () {
		return name
  }
}

const closureFunc = func();
closureFunc();

注意: 闭包的本质是利用了作用域的机制,来达到外部作用域访问内部作用域的目的。

闭包有以下特性:

  • 闭包是一个函数,它是另一个函数(称容器函数)的执行结果。

  • 闭包可以拥有其容器函数的作用域。

如何编写高性能的 JavaScript?

在编写 Javascript 代码时,若能遵循 Javascript 规范和注意一些性能方面的知识,对于提升代码的可维护性和优化性能将大有好处:

  • Js 文件加载和执行:

    • <script> 标签放到 <body> 标签的底部;

    • 合并多个 Js 文件,减少页面中 <script> 标签

    • 使用 defer 属性;

    • 使用 async 属性;

    • 动态加载脚本,Js 文件的下载和执行过程都不会阻塞页面其它进程。

  • 避免过长原型链继承;

  • 避免使用过深的嵌套循环;

  • 避免循环引用,防止内存泄漏;

  • 使用位运算代替一些简单的四则运算;

  • 不要使用未定义的变量;

  • 尽量少使用 eval;

  • 减少 DOM 操作,如:减少 DOM 访问次数、局部变量存储需要多次访问的 DOM、使用事件委托、减少重排和重绘等;

  • 当需要多次访问数组长度时,可以用变量保存起来,避免每次都会去进行属性查找;

  • 尽量不要用 for-in 循环去访问数组,建议用 for 循环进行循环:for(in) 的效率最差,因为它需要查询 Hash 键,for(;;)、while() 循环的性能基本持平;

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

“foo” 和“foobar” 等单词经常会作为示例名称出现在各种程序和技术文档中。据统计,在各种计算机和通信技术文档中,大约有百分之七的文档出现了这些词语。

“foo” 一词最早出现在一些二战前的漫画和卡通作品中。在关于消防队员的漫画 “Smokey Stover” 中,作者 Bill Holman 大量的使用了 “foo” 一词。这部漫画连载于 1930 至 1952 年,在其中,“foo” 这个词经常出现在汽车车牌上和一些没有意义的对话中, 例如:“He who foos last foos best” 或 “Many smoke but foo men chew” 等。根据作者自己介绍,“foo” 是根据某种中国的小雕像底部的文字得来的。这听起来很有道理,因为中国的雕像往往都会刻上祝福的文字,而 “foo” 可能是 “福” 的音译。不过英文文法能接纳 Holman 的 “foo” 这个编造的单词,也是受了犹太语中 “feh” 和英文中 “fooey” 及 “fool” 的影响。

在随后的二战中,因为英美士兵经常用俚语和粗话交流,“foo” 一词就被更广泛的传开了。人们把被雷达追踪到的神秘飞行物称为 “foo战机”(“foo fighter”),就是后来所谓的 UFO。而当英国士兵回防时,往往也会刻下 “foo到此一游”(“Foo was here.”)的字眼。

而关于“foobar”,也可以追溯到二战时期军队中的粗话缩写 “FUBAR”,含义是 ”一团糟,无法修复”(Fucked Up Beyond All Repair),后来演变成了 “foobar”。

从技术上讲,“foobar” 很可能在 1960 年代至 1970 年代初通过迪吉多的系统手册传播开来。另一种说法是,“foobar” 可能来源于电子学中反转的 foo 信号;这是因为如果一个数字信号是低电平有效,那么在信号标记上方一般会标有一根水平横线,而横线的英文即为 “bar”。在《新黑客辞典》中,还提到“foo”可能早于“FUBAR”出现

类似与 "helloworld" 用于示例中的输出文本,Foobar 经常被作为示例中的变量名单独使用,而当需要多个变量名时,foo、bar、baz 则经常被按顺序使用。

如何生成随机数?

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)

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

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

参考资料

扒一扒随机数(Random Number)的诞生历史

网上常能见到的一段 JS 随机数生成算法如下,为什么用 9301, 49297, 233280 这三个数字做基数?

JavaScript深入理解之对象创建

JavaScript深入理解之继承

阮一峰 Javascript 严格模式详解

JS 异步编程都有哪些解决方案?

最后更新于