三刷红宝书之变量和作用域

2,070 阅读6分钟

前言

正如标题所说,这是我第三次翻开红宝书也就是《 JavaScript 高级程序设计第三版》,不得不说,虽然书有一些年份,很多知识点也不适合现代的前端开发,但是对于想要掌握 JavaScript 基础的前端新手,亦或是像我一样想找回曾经遗忘在记忆角落的那些碎片知识,这本书依旧非常的适合,不愧被成为 "JavaScript 圣经"

本文是读书笔记,之所以又一次选择读这本书还有一个理由,之前都是记的纸质笔记,这次想把它作为电子版,也算是对之前知识的整理

本文篇幅较长,目的是作为我的电子版学习笔记,我会尽可能去其糟粕,取其精华,同时我会添加一些书上未记载但很重要的知识点补充

上篇在这里

基本类型和引用类型

JavaScript 不允许直接访问内存中的位置也就是说不能直接操作对象的内存空间

JS 只能通过指针(保存在栈中的变量)去操作堆内存中的引用类型的值即对象,如果给变量赋值另一个保存引用类型的值的变量,实际上只是创建了一个新指针,指向同一个堆内存中的对象

let obj = {}
let o = obj

obj.a = "123"

console.log(obj) // {a:"123"}
console.log(o)// {a:"123"}

在上一章末尾也提到过,这就会导致通过其中一个变量修改引用类型的值时,会反映到所有指向它的变量

而如果是基本类型的值,则会直接创建一个新的副本,意味着两者不会相互影响

当了解了这个知识点就可以知道为什么 JavaScript 会有深拷贝和浅拷贝这 2 个概念了,两者都是作用与引用类型

  • 浅拷贝会创建一个新对象,并且将原对象的根属性和值赋值给新对象,但是对于属性值仍是引用类型的属性则指向的还是同一个对象
  • 深拷贝会通过递归的方式拷贝每一层属性,从而使得拷贝后的对象和原对象不会相互影响

扩展运算符和 JSON.stringify 是比较常见的拷贝函数,前者用于浅拷贝,后者用于深拷贝

let arr = [1,2,3]
let shallowArr = [...arr]
let deepArr = JSON.parse(JSON.stringify(arr))

更多关于拷贝的知识点可以看我另一篇博客对象深拷贝和浅拷贝

检测类型

typeof 操作符可以很简单的确定变量是否是基本类型,但是对于引用类型会始终返回 'object' 或 'function'

对于进一步知道变量是哪种引用类型,需要使用 instanceof 操作符

let arr = []

console.log(typeof arr) // 'object'
console.log(arr instanceof Array) // true 表示它是一个数组的引用类型

对于基本类型的变量使用 instanceof 会返回 false

let str = 'abc'
console.log(str instanceof String) // false

注意这里 String 并不是 string,它是一个字符串的包装类型,同样也是引用类型,由于是基本类型,所以即使是字符串的包装类型,也会返回 false

但是 instanceof 也有缺陷

let arr = []
console.log(arr instanceof Object) // true 

这里返回 true,这样就无法判断 arr 变量是数组类型还是对象类型,导致这样的原因是数组类型是继承自对象类型,所以 instanceof 无法判断变量是由子类实例化还是由父类实例化的,所以又有了第三种解决方案 Object.prototype.toString,它可以解决 instanceof 无法判断子类和父类的问题

let arr = []
console.log(Object.prototype.toString.call(arr)) // "[object Array]"

理想总是美好的,现实总是骨感的,虽然 Object.prototype.toString 解决了具体是那种引用类型的问题,但是又引入了另外一个问题

它无法判断是基本类型还是基本包装类型

let str = 'abc'
let objStr = new String("abc")
console.log(Object.prototype.toString.call(str)) // "[object String]"
console.log(Object.prototype.toString.call(objStr)) // "[object String]"

可以看到,基本类型和基本包装类型始终都返回 "[object String]" ,这样仍无法区分具体的类型,但是我们可以将它和 typeof 结合

const isType = variable => {
    let type = typeof variable
    if(type === 'object' || type === 'function'){
       return Object.prototype.toString.call(variable).match( /\[object (\w+)]/)[1]
    }else{
        return type
    }
}

console.log(isType('123')) // 'string'
console.log(isType(new String('123'))) // 'String'

执行环境及作用域

执行环境有时候也被称作上下文,每个函数都有自己的执行环境,执行函数时,会将执行的函数推入全局唯一的环境栈,同时创建变量对象的一个作用域链,作用域是一套规定 JS 引擎如何查找变量的规则,它是一种嵌套的结构,层层递进,形成了链

由于在 innerFunc 中并没有变量 a,JS 引擎就会沿着作用域链,找到保存在 func 中的变量 a (如果 func 中仍没有,则会去全局作用域中寻找,再没有则返回 undefined)

也有一种说法是在创建函数的同时会创建该函数的作用域链,在执行函数时,复制当前函数的作用域链,并将当前函数的活动对象放入作用域链的最前端,我们来看上述代码的打印结果

可以看到在打印 innerFunc 的时候,innerFunc 还没有执行,此时它的内部已经有 2 个作用域形成的作用域链了,而在运行时才将名为 innerFunc 函数的作用域 (local) 放到作用域链的最前端

目前的 JavaScript 有 3 种环境

  • 全局环境
  • 函数环境
  • eval 环境

有 3 种作用域

  • 全局作用域
  • 函数作用域
  • 块级作用域 (ES6+)

块级作用域

块级作用域通俗来说就是花括号中的代码,在 ES6 之前 JavaScript 没有块级作用域,那时只能使用 var 来声明变量,会有变量提升的效果 (将声明变量的行为,提升到当前环境创建时执行)

if(false) {
    var color = "blue"
}

console.log('color' in window) // true

即使不进入 if 语句也会在 window 上创建一个 color 属性,因为在全局环境创建时,就已经创建 color 变量了,然后再执行代码

而 ES6 后,使用 let/const 可以将其包裹的花括号成为块级作用域,并且使用 let/const 声明的变量不会有变量提升

if(false) {
    let color = "blue"
    var otherColor = "red"
}

console.log('color' in window) // false
console.log('otherColor' in window) // true

未完待续

参考资料

《JavaScript 高级程序设计第三版》

《你不知道的JavaScript》