Variable of JavaScript
— FE Tutorial — 1 min read
Table of Contents
数据类型
ECMAScript 标准规定了 7 种数据类型,其把这7种数据类型又分为两种:
基础类型(原始类型):
Null
Undefined
Boolean
:布尔
Number
:双精度浮点型,还有一些特殊值(-Infinity、+Infinity、NaN)
String
:字符串
Symbol
:一种实例是唯一且不可改变的数据类型(es6)
BigInt
:任意精度的大整数(es10)
引用类型(对象类型):
Object
:对象
Function
:函数
Array
:数组
……
基础类型与引用类型区别
栈(stack) 内存:
- 存储的值大小固定
- 空间较小
- 可以直接操作其保存的变量,运行效率高
- 由系统自动分配存储空间
堆(heap) 内存:
- 存储的值大小不定,可动态调整
- 空间较大,运行效率低
- 无法直接操作其内部存储,使用引用地址读取
- 通过代码进行分配空间
基础类型:
原始类型的值被直接存储在栈中,在变量定义时,栈就为其分配好了内存空间
由于栈中的内存空间的大小是固定的,那么注定了存储在栈中的变量就是不可变的
1var str = 'ahabhgk';2var slicedStr = str.slice(1)34console.log(str) // ahabhgk5console.log(slicedStr) // habhgk
1str += '6'2console.log(str); // ahabhgk6
引用类型:
引用类型的值实际存储在堆内存中,它在栈中只存储了一个固定长度的地址,这个地址指向堆内存中的值。
1var obj = { name: 'ahabhgk' }2var fn = function () {}3var arr = [1, 2, 3]
引用类型就不再具有不可变性了,我们可以轻易的改变它们:
1obj.name = "baha"2arr.splice(1, 1)34console.log(obj) // { name: 'baha' }5console.log(arr) // [1, 3]
console.log 输出什么?1var obj = { a: 'foo', b: 'bar' }2var copy = obj3copy.a = 'wala'4console.log(obj.a)
'wala'1var obj = { a: 'foo', b: 'bar' }2var copy = obj3copy.a = 'wala'4console.log(obj.a)
console.log 输出什么?1var obj1 = { a: 'foo', b: 'bar' }2var obj2 = { a: 'foo', b: 'bar' }3console.log(obj1 === obj2)
false1var obj1 = { a: 'foo', b: 'bar' }2var obj2 = { a: 'foo', b: 'bar' }3console.log(obj1 === obj2)
包装类型
为了便于操作基本类型值,ECMAScript还提供了几个特殊的引用类型,他们是基本类型的包装类型:
- Boolean
- Number
- String
引用类型和包装类型的主要区别就是对象的生存期,使用new操作符创建的引用类型的实例,在执行流离开当前作用域之前都一直保存在内存中,而自基本类型则只存在于一行代码的执行瞬间,然后立即被销毁,这意味着我们不能在运行时为基本类型添加属性和方法。
装箱和拆箱
装箱转换:把基本类型转换为对应的包装类型
拆箱操作:把引用类型转换为基本类型
既然原始类型不能扩展属性和方法,那么我们是如何使用原始类型调用方法的呢?
装箱:
1var str = 'ahabhgk';2var slicedStr = str.slice(1)
实际上发生了以下几个过程:
- 创建一个 String 的包装类型实例
- 在实例上调用 slice 方法
- 销毁实例
也就是说,我们使用基本类型调用方法,就会自动进行装箱和拆箱操作,相同的,我们使用 Number 和 Boolean 类型时,也会发生这个过程。
拆箱:
从引用类型到基本类型的转换,也就是拆箱的过程中,会遵循 ECMAScript 规范规定的 toPrimitive 原则,一般会调用引用类型的 valueOf 和 toString 方法,你也可以直接重写 toPeimitive 方法。一般转换成不同类型的值遵循的原则不同,例如:
- 引用类型转换为 Number 类型,先调用 valueOf,再调用 toString
- 引用类型转换为 String 类型,先调用 toString,再调用 valueOf
若valueOf和toString都不存在,或者没有返回基本类型,则抛出TypeError异常。
1const basketball = {2 [Symbol.toPrimitive]() { // toString3 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 = 12let 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, undefined2a = function () {}3a()4b = 1 // 1
两个 console.log 分别输出什么?1console.log(a)2var a = 13console.log(a)
undefined1console.log(a)2var a = 13console.log(a)
1
函数提升
1foo()2function foo() {}
等同于:
1function foo() {}2foo()
运行结果是什么?1bar()2var bar = () => console.log('foo')
报错 // Uncaught TypeError: bar is not a function1bar()2var bar = () => console.log('foo')
暂时性死区(es6)
JavaScript 引擎在扫描代码发现变量声明时,要么将它们提升到作用域顶部(遇到 var 声明),要么将声明放在 TDZ 中(遇到 let 和 const 声明)。访问 TDZ 中的变量会触发运行时错误。只有执行过变量声明语句后,变量才会从 TDZ 中移出,然后方可访问。
1console.log(a, b) // Uncaught ReferenceError2const a = 13let b = 2
const 本质
const实际上保证的,并不是变量的值不得改动,而是变量指向的那个内存地址所保存的数据不得改动。对于简单类型的数据(数值、字符串、布尔值),值就保存在变量指向的那个内存地址,因此等同于常量。但对于复合类型的数据(主要是对象和数组),变量指向的内存地址,保存的只是一个指向实际数据的指针,const只能保证这个指针是固定的(即总是指向另一个固定的地址),至于它指向的数据结构是不是可变的,就完全不能控制了
有兴趣的可以阅读 Airbnb 有关变量声明的规范,也可以用 airbnb 规范配置的 eslint 试着写写代码,看现在变量通常是怎样声明的。
函数传参
ECMAScript 中所有的函数的参数都是值传递
1var a = 12function setAddNum(num) { // var num = a3 var base = 04 return function add() {5 base += num6 return base7 }8}9var add = setAddNum(a)10var sum1 = add()11console.log(sum1) // 1
console.log 输出什么?1const o = {}2function changeValue(obj) { // var obj = o3 obj.name = 'ahabhgk'4 obj = { name: 'haha' }5}6changeValue(o)7console.log(o.name)
'ahabhgk'1const o = {}2function changeValue(obj) { // var obj = o3 obj.name = 'ahabhgk'4 obj = { name: 'haha' }5}6changeValue(o)7console.log(o.name)
类型判断
typeof
1typeof 'ahabhgk' // string2typeof 123 // number3typeof true // boolean4typeof Symbol() // symbol5typeof undefined // undefined6typeof () => {} // function78typeof null // object9typeof [] // object10typeof {} // object11typeof new Date() // object12typeof /^\d*$/ // object
除函数外所有的引用类型都会被判定为 object
基本类型中 null 会判定为 object,这是在JavaScript初版就流传下来的bug,后面由于修改会造成大量的兼容问题就一直没有被修复
instance of
根据原型链查找
1[] instanceof Array // true2new Date() instanceof Date // true3new RegExp() instanceof RegExp // true45[] instanceof Object // true6function () {} 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}67const isArray = isType('Array')89isArray([]) // true10isArray({}) // 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 i34i = 05setTimeout(function () {6 console.log(i)7}, 0)89i++10setTimeout(function () {11 console.log(i)12}, 0)1314i++15setTimeout(function () {16 console.log(i)17}, 0)1819i++20setTimeout(function () {21 console.log(i)22}, 0)2324i++25setTimeout(function () {26 console.log(i)27}, 0)2829i++
解决方法:
- 函数的值传递
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}
- 使用块级作用域(es6)
1for (let i = 0; i < 5; i++) {2 setTimeout(function () {3 console.log(i)4 }, 0)5}
1// 伪代码2{3 let i = 04 setTimeout(function () {5 console.log(i)6 }, 0)7}89{10 let i = 111 setTimeout(function () {12 console.log(i)13 }, 0)14}1516{17 let i = 218 setTimeout(function () {19 console.log(i)20 }, 0)21}2223{24 let i = 325 setTimeout(function () {26 console.log(i)27 }, 0)28}2930{31 let i = 432 setTimeout(function () {33 console.log(i)34 }, 0)35}3637{38 let i = 539}
END
只是摘出来一部分(如果代码写的规范的话,很多部分都遇不到),更具体的看这篇:你真的掌握变量和类型了吗?
Think:
学习闭包 虽然可能很长时间都不会用到,但闭包在 JavaScript 是很重要的,闭包学得好不仅可以为以后函数式编程、学习框架等打下基础,还可以通过闭包理解很多东西:
- 静态(词法)作用域与动态作用域
- 作用域链
- 执行上下文栈 ……
写一个 val 函数,实现:
1function val(value) {2 //3}45const one = val(1)6const three = one.plus(2)7const six = three.plus(3)89console.log(one.value) // 110console.log(three.value) // 311console.log(six.value) // 61function val(value) {2 function plus(num) {3 value += num4 return { plus, value }5 }6 return { plus, value }7}写一个 add 函数,实现:
1const add = function (a) {2 //3}45add(1) // 16add(1)(2) // 37add(1)(2)(3) // 68add(1)(2)(3)(4) // 10提示:重写 toString 方法
1const add = function (value) {2 function sum(num) {3 value += num4 return sum5 }6 sum.toString = function () {7 return value8 }9 return sum10}
推荐: