Weekbin

一个厉害的前端新手

LV0
已经发布0篇文章,距离下一等级还需发布1篇文章

JavaScript 浅析(一)

一、变量

1. 变量声明

在 JavaScript 中,总共有三种关键字可以声明变量:var、const 和 let。其中 const 和 let 只能在 ES6+ 的版本中使用。
在条件允许的情况下不要使用 var,因为 let 和 const 可以完成 var 的功能;const 优先, let 次之。

  1. 变量提升使用 var 时,在变量声明之前使用变量不会报错,只会返回 undefined。这是因为使用这个关键字声明的变量会自动提升到函数作用域顶部。

  2. 暂时性死区使用 let 与 var 的一个重要区别就是,let 声明的代码不会在作用域中被提升。

  3. 作用域 let 的作用域为代码块,可以简单理解为大括号;而 var 的作用域为函数,作用域为整个 function 内部。

  4. 全局声明 var 在浏览器环境下的全局声明会绑定到 window 对象上,而 let 则不会。

  5. const 声明 const 声明的行为和 let 基本相同,但是 const 声明时的指针是不能被改变的,也就是说 const 的内容如果是基本类型,那么这个值的内容不能被改变 ;如果声明的是一个复杂类型,那么 const 声明的变量不能将指针引向其他对象,但是修改指针所指的当前对象的属性或者方法是被允许的。

2. 数据类型

ES 总共有 6 种简单数据类型:Undefined、Null、Boolean、Number、String 和 Symbol。还有一种复杂数据类型叫做 Object,属于一种无序名值对的集合。在 JS 中, 所有的数据类型都由以上七种类型组成。

Undefined

undefined 永远为假值,当变量或属性不存在、未声明时会返回 undefined。不指定返回值的函数实际上会返回特殊值 undefined。

Null

null 和 undefined 不同,null 表示一个空指针,所以用 typeof 来判断 会返回 "Object"。

Boolean

数据类型truefalse
Booleantruefalse
String非空字符串空字符串
Number非零数值0、NaN
Object任意对象null
Undefined不存在永远

Number

当一个数值本应当为数字,但是实际不是数字时会返回 NaN,同时 NaN 做任何有关数字的运算都会返回 NaN(所以平时开发时并没有明显的异常被抛出)。
总共有三个方法可以将非数值转换为数值,分别是: Nubmer()、parseInt()、parseFloat(),其中 parseInt 接受第二个参数,表示解析的数字应当为什么进制,通常不 输入任何值的情况下默认是 10,也就是转换为十进制整数。

复制
let num1 = parseInt('10', 2) // 2,按二进制解析
let num2 = parseInt('10', 8) // 8,按八进制解析
let num3 = parseInt('10', 10) // 10,按十进制解析
let num4 = parseInt('10', 16) // 16,按十六进制解析

【注】与其他语言不同, JS 不区分整数和浮点数,只有 Number 一种数值数据类型。

String

ES6 中有一个新特性就是模板字符串,其中还有一个骚操作就是模板字面量标签函数(虽然平时并没怎么用到,似乎没有什么用)。

复制
let a = 'hello'
let b = 'world'
function Tag(strings, ...args) {
  console.log(strings)
  args.forEach(arg => {
    console.log(arg)
  })
  return 'foo'
}

console.log(Tag`${a} ${b}`)
// ["", " ", "", raw: Array(3)]
// hello
// world
// foo

Symbol

Symbol 是 ES6 新增的数据类型。符号是原始值,且符号实例时唯一、不可变的。符号与其他基本类型不同,它没有包装对象,不可以使用构造函数,不能使用 new,下面 的代码会报错:

复制
let boolean = new Boolean() // Boolean {fales}

let string = new String() // String {""}

let number = new Number() // Number {0}

let symbol = new Symbol() // TypeError: Symbol is not a constructor

Symbol 可以作为使用属性使用,理论上凡是可以使用字符串或数值当作属性的地方,都可以使用 Symbol。

复制
let a = Symbol('a')
let b = Symbol('b')
let o = {
  a: 'hello',
  b: 'world'
}

Object.assign(o, {
  [a]: 'foo'
})

Object.defineProperty(o, b, { value: 'baz' })

console.log(o) // {a: "hello", b: "world", Symbol(a): "foo", Symbol(b): "baz"}

Object.getOwnPropertyNames()返回对象实例的常规属性数组,Object.getOwnPropertySymbols()返回对象实例的符号属性数组,Object.getOwnPropertyDescriptors()会 返回同时包含常规和符号属性描述符的对象,Reflect.ownKeys()会返回两种类型的键。

复制
console.log(Object.getOwnPropertySymbols(o))
// [Symbol(a), Symbol(b)]

console.log(Object.getOwnPropertyNames(o))
// ["a", "b"]

console.log(Object.getOwnPropertyDescriptors(o))
// {a: {...}, b: {...}, Symbol(a): {...}, Symbol(b): {...}}

console.log(Reflect.ownKeys(o))
// ["a", "b", Symbol(a), Symbol(b)]

Symbol 除此之外还作为属性表示一些内部方法(内置符号)

Object

JS 中的 Object 由上面 6 种基本属性组成。

  1. valueOf、toString 和 toLocaleString 的区别 valueOf 返回对象的原始值, toString 和 toLocaleString 都会返回字符串,但是 toLocaleString 返回的是本地环 境字符串。所以保险起见,尽量不要使用 toLocaleString。
复制
const a = new Array(1, 2, 3)
const b = new Object({ a: 1, b: 2, c: 3 })
const c = new Date()
const d = new Boolean(false)
const e = new Number(12343)

a.valueOf() // [1, 2, 3]
b.valueOf() // {a: 1, b: 2, c: 3}
c.valueOf() // 1611194878248
d.valueOf() // false
e.valueOf() // 12343

a.toString() // "1,2,3"
b.toString() // "[object Object]"
c.toString() // "Thu Jan 21 2021 10:07:58 GMT+0800 (中国标准时间)"
d.toString() // "false"
e.toString() // "12343"

a.toLocaleString() // "1,2,3"
b.toLocaleString() // "[object Object]"
c.toLocaleString() // "Thu Jan 21 2021"
d.toLocaleString() // "false"
e.toLocaleString() // "12,343"

3. 操作符

位操作符

  1. ~ 按位非 ~ 的操作会让数值结果取反并减一(丢失精度),~~ 的操作相当于 Math.floor()。
复制
console.log(~26.1) // -27
console.log(~~26.1) // 26
  1. & 按位与 & 的操作就是将两个数(二进制)按照位对其,然后做与运算(全零为零,全一为一,其他为零)。

  2. | 按位或同按位与类似,将两个数(二进制)按照位对其,然后做位运算(全零为零,有一为一)。

  3. ^ 按位异或类似的,将两个数(二进制)按照位对其,然后做异或运算(全零、全一为零,反之为一)。

  4. << 左移

复制
let v = 2 // 二进制 10 => 十进制 2
let n = 2 << 5 // 二进制 1000000 => 十进制 64

左移操作会以 0 填补右侧的空位,且会保留操作值的符号,如果是 -2 左移 5 位,结果是 -64。

  1. >> 有符号右移与左移相反,以 0 填补左侧的空位,且保留操作值的符号。

  2. >>> 无符号右移

复制
let n = -64 // 等于二进制 11111111111111111111111111000000
let v = n >>> 5 // 等于十进制 134217726

布尔操作符

  1. ! 逻辑非通常来说 !! 操作与 Boolean() 的结果相同,相当于将一个值强制转换为布尔值。

  2. && 逻辑与如果第一个操作数为真值,则返回第二个操作数,反之直接返回第一个操作数。通常用于属性判断的短路操作,确保程序不报错。

复制
let o = {}
o.name // undefined
o.name.firstName // Cannot read property 'a' of undefined

o.name && o.name.firstName // undefined
  1. || 逻辑或如果第一个操作数为真值则直接返回,反之返回第二个操作数。通常用于初始化值,或者防止类型强转。
复制
let n = numberFromAjaxResponse || 0 // 当 AjaxResponse 不存在时,初始化值为 0,防止值变成 undefined

相等操作符

  1. == 和 != 等于和不等于值得注意的是,当比较对象与基本类型是否相同时,与 <、<=、>、>= 的运算规则相同,遇到对象会首先调用 valueOf() 方法去尝试取值 。
复制
const a = {
  valueOf() {
    return 'a'
  }
}

class P {
  valueOf() {
    return 'a'
  }
}
const p = new P()

a == 'a' // true
p == 'a' // true

有趣的是,null 和 undefined 相等,NaN 和 NaN 不相等,两个对象相比较,并不会调用 valueOf 去比较值,当且仅当他们的指针指向同一个对象时才为 true。

复制
null == undefined // true
NaN == NaN // false

const a = {
  valueOf() {
    return 'a'
  }
}
let b = {
  valueOf() {
    return 'a'
  }
}

a == b // false
b = a
a == b // true

undefined 和 null 和任何值都不相等

  1. === 和 !== 全等和不全等全等和相等操作类似,只不过在比较的时候不会进行操作数的强制类型转换,所以当两个操作数类型不同时,返回值必然为 false。在实际 开发中还是推荐使用 全等与不全等 来进行类型比较。

4. 语法

ES6 中比较有趣的是 for-in 语句 和 for-of 语句。

for-in 语句默认返回的是对象的 key

复制
for (const key in { a: 1, b: 2, c: 3 }) {
  console.log(key)
}
// a
// b
// c
for (const key in ['a', 'b', 'c']) {
  console.log(key)
}
// 0
// 1
// 2

for-of 语句默认返回 可迭代对象 的 value,如果想得对象的 key-value, 可以使用 for-of, 此处其实是将对象先转化为可迭代对象,然后解构其参数获得 key, value。

复制
for (const [a, b] of { a: 1, b: 2, c: 3 }) {
  console.log(a, b)
}
// {(intermediate value)(intermediate value)(intermediate value)} is not iterable

for (const [key, value] of Object.entires({ a: 1, b: 2, c: 3 })) {
  console.log(key, value)
}
// a
// b
// c
for (const [key, value] of Object.entires(['a', 'b', 'c'])) {
  console.log(key, value)
}
// 0
// 1
// 2

5. 函数

ES6 中可以不指定参数个数,相较于 arguments 更加灵活。

复制
function test(a, b, ...args) {
  console.log(arguments)
  console.log(args)
  console.log(arguments instanceof Array)
  console.log(args instanceof Array)
}
test('a', 'b', 'c', 'd', 'e')
// Arguments(5) ["a", "b", "c", "d", "e"] 值是一个类数组
// (3) ["c", "d", "e"] 值是一个数组
// false
// true

将类数组转换为数组

复制
1. Array.prototype.slice.call(arguments)

2. Array.from(arguments)

3. [...arguments]

6. 垃圾回收

标记清理

标记清理的思想就是根据执行上下文来进行垃圾回收,如果变量在执行作用域中,那么这个变量就不应该被清理,反之如果这个变量不在执行作用域中,说明可以被清理。 目前,IE、Firefox、Opera、Chrome、Safari 等主流浏览器都在自己的 JS 实现中使用标记清理的方式进行垃圾回收(或其变体),只是在垃圾回收的频率上有所区别。

引用计数

引用计数的思路是对每个值都记录它被引用的次数,如果同一个值被赋给另一个变量,那么引用次数加一,反之,如果引动变量被其他值覆盖了,那么引用次数减一。当一 个值的引用次数为零时,说明没有变量可以访问到这个值了,因此可以安全地回收其内存了。

但是引用技术有一个很严重的问题:

复制
let a = new Object()
let b = new Object()

a.some = b
b.any = a

无论是 a 还是 b,引用次数永远都不会小于 2,使用引用计数的方法始终是无法清理循环引用的变量的。但是标记清理可以解决这个问题,标记清理会清理不在上下文中 用到的变量,当 a 和 b 不在执行上下文中,自然而然地就被自动回收了。

二、基本引用类型

1. Date

Date 接受两种方法进行初始化日期,字符串形式或数字形式,两者在内部会被 Date.parse() 或 Date.UTC 隐式调用。

复制
let d1 = new Date('May 23, 2019')
let d1 = new Date(2005, 4, 5, 17, 55, 55) // 本地时间 2005年5月5日下午5点55分55秒

可以使用 getFullYear()、getMonth()、getDate()、getDay()、getHours()、getMinutes()、getSeconds()、getMilliseconds() 等方法对日期进行格式化操作。

2. RegExp

RegExp 实例主要有两个方法:exec() 和 test(),前者用来配合捕获组使用,后者主要用于判断输入的文本与正则是否像匹配。

exec() 方法会返回一个类数组,里面包含所有捕获组和几个额外的属性(input、 index、 ?group)。

复制
let text = 'mom and dad and baby'
let pattern = /mom( and dad( and baby)?)?/gi

pattern.exec(text)
// ["mom and dad and baby", " and dad and baby", " and baby", index: 0, input: "mom and dad and baby", groups: undefined]

test() 方法不会返回数组,只会返回对于正则表达式的匹配结果,匹配成功是 true, 反之 false。

复制
let text = '000-00-0000'
let pattern = /\d{3}-\d{2}-\d{4}/

pattern.test(text)
// true

【注】即使 pattern 看上去像个字符串,但它绝对不是基本类型,而是一个 Object。

3. 包装对象

ES 提供了 3 种特殊的引用类型:Boolean、Number 和 String。先来看一个有意思的现象:

复制
let a = 'anban'
let b = a[0] // "a"
let c = a.slice(1) // "nban"

如上,变量 a 只是一个基本数据类型 “字符串”,但是第二行用下标拿属性的方法取到了字符串的值,第三行用调用方法的方式得到了返回结果 “nban”。如果这一切发生 在某个对象身上,这似乎看上去合情合理,但是 a 属于基本数据类型 “字符串”,这在逻辑上不合理,因为字符串不应该拥有方法和属性。而实际上这几行代码被正确执行 了,后台执行了以下三个步骤:

  1. 创建一个 String 类型的实例
  2. 调用实例上的特定方法或者获取属性
  3. 销毁实例

ES 的这种行为让原始对象拥有了对象的特征,对于 Boolean 和 Number 也是一样的,只不过使用的包装对象不通。

【注】引用类型和包装对象在生命周期上有本质的区别,可以简单理解为,以上三行代码的包装对象只存在于某一行。所以如果给字符串添加属性虽然不会报错,但是在之 后的代码中是不会存在的,因为那个包装对象已经销毁了。

Number

Boolean

String

4. 内置对象

Global

Global 对象不会被代码显式访问,事实上你可以想到的任何可以直接调用的方法都是 Global 对象的属性或方法。

在浏览器环境下,浏览器将 window 对象作为 Global 对象的代理,任何值都通过 window 来访问,除此之外,window 还有许多 Global 不具有的属性与方法(仅限浏览 器)。

Math

Math 总共有 8 种属性,35 种方法。

Math

三、集合引用类型

有两种方式可以声明集合引用类型:调用构造函数声明 和 使用字面量方式声明。与 new 方式声明相比字面量声明方式更加简洁明了。

【注】以字面量方式声明集合引用类型不会主动调用构造函数。

1. Object

Object 除了构造函数之外,也就只有 6 种常用方法,分别是:hasOwnProperty()、isPrototypeOf()、propertyIsEnumerable()、toLocalString()、toString() 和 valueOf。虽然其本身没有什么方法,但却常用于数据存储和交换。

Object

2. Array

ES 规定的 Array 是一组有序数据,每个槽位可以存储任意类型的数据,数据长度会随着数据添加会自动增长。ES6 中新增了两个数组方法:Array.from() 和 Array.of(),前者用于将类数组转换为数组,后者将一组参数转换为数组,后者可以替代较为笨拙的转换类数组的方法 Array.prototype.slice.call(arguments) 。

【注】尽量避免数组声明时存在空位,如 let a = [,,,],ES6 之前的解释方式和之后的解释方式不通,具体行为不同。数组的索引最长为 2^32 -1。

Object

迭代器方法

在 ES6 中,Array 的原型上也有 keys()、values() 和 entires() 三种方法,与 Object.keys()、Object.values() 和 Object.entires() 不同,其返回索引/值/索引和 值的迭代器。

【注】以上三种方法的使用需要注意浏览器兼容性。

基本方法

MDN JavaScript 标准内置对象 Array

push()、pop()、shift()、unshift()、reverse()、sort()、contact()、indexOf()、lastIndexOf、include()、find()、findIndex()、every()、filter()、forEach()、map()、some()、reduce()、reduceRight()

push()、pop()、shift()、unshift() 可以模拟队列或栈的行为。

reverse() 和 sort() 方法返回的都是对数组的引用,它们都会改变原数组。

indexOf()、lastIndexOf、include()、find()、findIndex() 一般用于判断值是否在数组中。

every()、filter()、forEach()、map()、some()、reduce()、reduceRight() 方法用于数据处理。

数组方法中只有 7 种方法会对原数组进行改变,其分别是:pop()、push()、shift()、unshift()、sort()、reverse() 和 splice()。其他方法都会返回一个处理后的新 对象。

3. 定型数组

contact()、pop()、push()、shift()、unshift()、splice() 不适用于定型数组。

set() 和 subarray() 是特有的方法,set 用于改变特定索引开始的值,subarray 用于复制原定型数组,拷贝一份新的。

ArrayBuffer

Object

DataView

定型数组

4. Map

MDN JavaScript 标准内置对象 Map

Object

与 Object 只能用数值、字符串或符号作为 key 不同,Map 可以用任何 JS 的数据类型作为 key,key 的比较是严格的,可以理解为 ===,但也有区别。

复制
const x = new Map()
const a = NaN,
  b = NaN,
  c = +0,
  d = -0

a === b // false
u === v // true

x.set(a, 'hello')
x.set(c, 'world')

x.get(b) // "hello"
x.get(d) // "world"

与 Object 不同,map 是支持顺序迭代的,也就是说,值的顺序与插入的顺序相同。

对于业务开发来讲,无论选择 Object 还是 Map 都可以完成大部分数据交换的操作,但在以下几个方面存在区别:

  1. 内存占用通常来说,Map 更节省空间。

  2. 插入性能通常来说,Map 性能更佳。

  3. 查找速度如果代码涉及大量查找,Object 更好。

  4. 删除性能通常来说,Map 合适。毕竟在 Object 上使用 delete 操作,或者将值设为 null 或 undefined 都不是非常合适。

5. WeakMap

MDN JavaScript 标准内置对象 WeakMap

Object

WeakMap 种的 key 只能是 Object 或者继承于 Object 的类型(使用原始值的包装对象是允许的),value 的值的类型没有限制。与 Map 相同,key/value 也是顺序的。

【注】WeakMap 本身就算存在,也不会阻止垃圾回收。使用 WeakMap 时,只要 key/value 存在,就无需担心会被垃圾回收。但一定要有外部变量的指针指向 key,不然就 会被垃圾回收。

弱映射可以储存与 dom 相关的信息,如果某个 Tag 被 JS 从 DOM 种移除了,那么这个若映射就会自动被垃圾回收回收了;如果使用 Map(),则引用会始终存在,也就不 会被垃圾回收了。

复制
const m = new Map()

const loginButton = document.querySelector('#login')

m.set(loginButton, { disabled: true })

const wm = new WeakMap()

const loginButton = document.querySelector('#login')

wm.set(loginButton, { disabled: true })

6. Set

MDN JavaScript 标准内置对象 Set

Object

7. WeakSet

MDN JavaScript 标准内置对象 WeakSet

Object

WeakMap 中的值只能是 Object 或者继承于 Object 的类型(原始类型的包装对象是允许的)。与 WeakMap 相同,WeakMap 中的值必须被别的变量引用,否则就会被垃圾 回收;与 WeakMap 相同,WeakSet 同样也是不可迭代的;与 WeakMap 类似,WeakSet 可以对 DOM 元素打些标签,DOM 元素被删除后,WeakSet 会自动被垃圾回收。

7. 总结

  1. Object 是一个基础类型,JS 中所有对象都继承于它。
  2. 原始类型(string、boolean 和 number)可以使用属性和方法是因为 JS 在解释的时候会调用其包装对象,从而具有对象的行为和特征。
  3. Array、定性数组、Map 和 Set 都是可迭代的,这意味着它们都能顺序迭代,也就可以用 for-of 来遍历;也都支持 ... 拓展操作符(浅复制);也都可以将可迭代对 象传入自身的构造函数进行浅复制。
  4. Math、RegExp 和 Date 对象都可以在全局环境下使用。
up-to-top