Skip to content

AHABHGK

Variable of JavaScript

FE Tutorial1 min read

Table of Contents

数据类型

ECMAScript 标准规定了 7 种数据类型,其把这7种数据类型又分为两种:

基础类型(原始类型):

Null

Undefined

Boolean:布尔

Number:双精度浮点型,还有一些特殊值(-Infinity、+Infinity、NaN)

String:字符串

Symbol:一种实例是唯一且不可改变的数据类型(es6)

BigInt:任意精度的大整数(es10)

0-null-undefined.png

引用类型(对象类型):

Object:对象

Function:函数

Array:数组

……

基础类型与引用类型区别

栈(stack) 内存:

  • 存储的值大小固定
  • 空间较小
  • 可以直接操作其保存的变量,运行效率高
  • 由系统自动分配存储空间

堆(heap) 内存:

  • 存储的值大小不定,可动态调整
  • 空间较大,运行效率低
  • 无法直接操作其内部存储,使用引用地址读取
  • 通过代码进行分配空间

基础类型:

原始类型的值被直接存储在栈中,在变量定义时,栈就为其分配好了内存空间

primitive-values.png

由于栈中的内存空间的大小是固定的,那么注定了存储在栈中的变量就是不可变的

1var str = 'ahabhgk';
2var slicedStr = str.slice(1)
3
4console.log(str) // ahabhgk
5console.log(slicedStr) // habhgk
1str += '6'
2console.log(str); // ahabhgk6

str.png

引用类型:

引用类型的值实际存储在堆内存中,它在栈中只存储了一个固定长度的地址,这个地址指向堆内存中的值。

1var obj = { name: 'ahabhgk' }
2var fn = function () {}
3var arr = [1, 2, 3]

ref-values.png

引用类型就不再具有不可变性了,我们可以轻易的改变它们:

1obj.name = "baha"
2arr.splice(1, 1)
3
4console.log(obj) // { name: 'baha' }
5console.log(arr) // [1, 3]
console.log 输出什么?
1var obj = { a: 'foo', b: 'bar' }
2var copy = obj
3copy.a = 'wala'
4console.log(obj.a)
'wala'
console.log 输出什么?
1var obj1 = { a: 'foo', b: 'bar' }
2var obj2 = { a: 'foo', b: 'bar' }
3console.log(obj1 === obj2)
false

包装类型

为了便于操作基本类型值,ECMAScript还提供了几个特殊的引用类型,他们是基本类型的包装类型:

  • Boolean
  • Number
  • String

引用类型和包装类型的主要区别就是对象的生存期,使用new操作符创建的引用类型的实例,在执行流离开当前作用域之前都一直保存在内存中,而自基本类型则只存在于一行代码的执行瞬间,然后立即被销毁,这意味着我们不能在运行时为基本类型添加属性和方法。

装箱和拆箱

装箱转换:把基本类型转换为对应的包装类型

拆箱操作:把引用类型转换为基本类型

既然原始类型不能扩展属性和方法,那么我们是如何使用原始类型调用方法的呢?

装箱:

1var str = 'ahabhgk';
2var slicedStr = str.slice(1)

实际上发生了以下几个过程:

  1. 创建一个 String 的包装类型实例
  2. 在实例上调用 slice 方法
  3. 销毁实例

也就是说,我们使用基本类型调用方法,就会自动进行装箱和拆箱操作,相同的,我们使用 Number 和 Boolean 类型时,也会发生这个过程。

拆箱:

从引用类型到基本类型的转换,也就是拆箱的过程中,会遵循 ECMAScript 规范规定的 toPrimitive 原则,一般会调用引用类型的 valueOf 和 toString 方法,你也可以直接重写 toPeimitive 方法。一般转换成不同类型的值遵循的原则不同,例如:

  • 引用类型转换为 Number 类型,先调用 valueOf,再调用 toString
  • 引用类型转换为 String 类型,先调用 toString,再调用 valueOf

若valueOf和toString都不存在,或者没有返回基本类型,则抛出TypeError异常。

1const basketball = {
2 [Symbol.toPrimitive]() { // toString
3 return '🏀'
4 },
5}
6console.log(basketball == '🏀')
7console.log(basketball === '🏀')
8console.log('你打' + basketball + '……')
一道经典的面试题,如何让:a == 1 && a == 2 && a == 3 返回 true。根据上面的拆箱转换;同时使用 == 时,若两侧类型相同,则比较结果和 === 相同,否则会发生隐式转换,所以:
1const a = {
2 value: [3, 2, 1],
3 valueOf() {
4 return this.value.pop()
5 },
6}

变量声明

1var a = 1
2let b = 'hah'
3const obj = {}
4const foo = (...args) => {
5 //
6}
7function bar() {
8 //
9}
10class Person {
11 //
12}
13import img from './img.png'

变量提升

1var a = function () {} // 以变量形式定义函数
2a()
3var b = 1

等同于:

1var a, b // undefined, undefined
2a = function () {}
3a()
4b = 1 // 1
两个 console.log 分别输出什么?
1console.log(a)
2var a = 1
3console.log(a)
undefined

1

函数提升

1foo()
2function foo() {}

等同于:

1function foo() {}
2foo()
运行结果是什么?
1bar()
2var bar = () => console.log('foo')
报错 // Uncaught TypeError: bar is not a function

暂时性死区(es6)

JavaScript 引擎在扫描代码发现变量声明时,要么将它们提升到作用域顶部(遇到 var 声明),要么将声明放在 TDZ 中(遇到 let 和 const 声明)。访问 TDZ 中的变量会触发运行时错误。只有执行过变量声明语句后,变量才会从 TDZ 中移出,然后方可访问。

1console.log(a, b) // Uncaught ReferenceError
2const a = 1
3let b = 2

const 本质

const实际上保证的,并不是变量的值不得改动,而是变量指向的那个内存地址所保存的数据不得改动。对于简单类型的数据(数值、字符串、布尔值),值就保存在变量指向的那个内存地址,因此等同于常量。但对于复合类型的数据(主要是对象和数组),变量指向的内存地址,保存的只是一个指向实际数据的指针,const只能保证这个指针是固定的(即总是指向另一个固定的地址),至于它指向的数据结构是不是可变的,就完全不能控制了

有兴趣的可以阅读 Airbnb 有关变量声明的规范,也可以用 airbnb 规范配置的 eslint 试着写写代码,看现在变量通常是怎样声明的。

函数传参

ECMAScript 中所有的函数的参数都是值传递

1var a = 1
2function setAddNum(num) { // var num = a
3 var base = 0
4 return function add() {
5 base += num
6 return base
7 }
8}
9var add = setAddNum(a)
10var sum1 = add()
11console.log(sum1) // 1
console.log 输出什么?
1const o = {}
2function changeValue(obj) { // var obj = o
3 obj.name = 'ahabhgk'
4 obj = { name: 'haha' }
5}
6changeValue(o)
7console.log(o.name)
'ahabhgk'

类型判断

typeof

1typeof 'ahabhgk' // string
2typeof 123 // number
3typeof true // boolean
4typeof Symbol() // symbol
5typeof undefined // undefined
6typeof () => {} // function
7
8typeof null // object
9typeof [] // object
10typeof {} // object
11typeof new Date() // object
12typeof /^\d*$/ // object

除函数外所有的引用类型都会被判定为 object

基本类型中 null 会判定为 object,这是在JavaScript初版就流传下来的bug,后面由于修改会造成大量的兼容问题就一直没有被修复

instance of

根据原型链查找

1[] instanceof Array // true
2new Date() instanceof Date // true
3new RegExp() instanceof RegExp // true
4
5[] instanceof Object // true
6function () {} instanceof Object // true

Object.prototype.toString

1const isType = function (type) {
2 return function (instance) {
3 return Object.prototype.toString.call(instance) === `[object ${type}]`
4 }
5}
6
7const isArray = isType('Array')
8
9isArray([]) // true
10isArray({}) // false

作用域

  • 全局作用域(尽量避免污染 :poop:全局作用域)
  • 函数作用域
  • 块级作用域(es6)
1for (var i = 0; i < 5; i++) {
2 setTimeout(function () {
3 console.log(i)
4 }, 0)
5}
6// 5 个 5
1// 伪代码
2var i
3
4i = 0
5setTimeout(function () {
6 console.log(i)
7}, 0)
8
9i++
10setTimeout(function () {
11 console.log(i)
12}, 0)
13
14i++
15setTimeout(function () {
16 console.log(i)
17}, 0)
18
19i++
20setTimeout(function () {
21 console.log(i)
22}, 0)
23
24i++
25setTimeout(function () {
26 console.log(i)
27}, 0)
28
29i++

解决方法:

  1. 函数的值传递
1for (var i = 0; i < 5; i++) {
2 (function (i) {
3 setTimeout(function () {
4 console.log(i)
5 }, 0)
6 })(i)
7}

仔细看 setTimeout 的 API 文档,改进:

1for (var i = 0; i < 5; i++) {
2 setTimeout(function (j) {
3 console.log(j)
4 }, 0, i)
5}
  1. 使用块级作用域(es6)
1for (let i = 0; i < 5; i++) {
2 setTimeout(function () {
3 console.log(i)
4 }, 0)
5}
1// 伪代码
2{
3 let i = 0
4 setTimeout(function () {
5 console.log(i)
6 }, 0)
7}
8
9{
10 let i = 1
11 setTimeout(function () {
12 console.log(i)
13 }, 0)
14}
15
16{
17 let i = 2
18 setTimeout(function () {
19 console.log(i)
20 }, 0)
21}
22
23{
24 let i = 3
25 setTimeout(function () {
26 console.log(i)
27 }, 0)
28}
29
30{
31 let i = 4
32 setTimeout(function () {
33 console.log(i)
34 }, 0)
35}
36
37{
38 let i = 5
39}

END

只是摘出来一部分(如果代码写的规范的话,很多部分都遇不到),更具体的看这篇:你真的掌握变量和类型了吗?

Think:

  1. 学习闭包 虽然可能很长时间都不会用到,但闭包在 JavaScript 是很重要的,闭包学得好不仅可以为以后函数式编程、学习框架等打下基础,还可以通过闭包理解很多东西:

    • 静态(词法)作用域与动态作用域
    • 作用域链
    • 执行上下文栈 ……
  2. 写一个 val 函数,实现:
    1function val(value) {
    2 //
    3}
    4
    5const one = val(1)
    6const three = one.plus(2)
    7const six = three.plus(3)
    8
    9console.log(one.value) // 1
    10console.log(three.value) // 3
    11console.log(six.value) // 6
    1function val(value) {
    2 function plus(num) {
    3 value += num
    4 return { plus, value }
    5 }
    6 return { plus, value }
    7}
  3. 写一个 add 函数,实现:
    1const add = function (a) {
    2 //
    3}
    4
    5add(1) // 1
    6add(1)(2) // 3
    7add(1)(2)(3) // 6
    8add(1)(2)(3)(4) // 10

    提示:重写 toString 方法

    1const add = function (value) {
    2 function sum(num) {
    3 value += num
    4 return sum
    5 }
    6 sum.toString = function () {
    7 return value
    8 }
    9 return sum
    10}

推荐: