前言
最近开始入坑js“圣经”之一的《JavaScript高级程序设计》,准备深入学习这门“博大精深”的语言,下面我学习第四章的总结及拓展。
变量
js中声明变量的几种方式
下面是es5以及之前的方式(var,function):
- var:可以声明任意类型的变量,js会根据变量的值来自动判定这个值是什么类型。
- function:主要是声明函数类型的变量,如定义一个函数。
- 啥也不加直接写变量。这种方式是创建了一个全局变量,而且无法被垃圾回收机制回收,只有退出这个变量所在的环境后,这个变量才会被销毁,如关闭浏览器
下面是es6声明变量的方式(let,const,import,class)
- let:和var作用一样,只是该变量被限制在了当前代码块(当前作用域)
基本用法:
// var是没有块级作用域的概念
{
let a = 10;
var b = 1;
}
a // ReferenceError: a is not defined.
b // 1
还有一个更经典的例子:
for (var i = 0; i < 10; i++) {
doSomething(i);
}
alert(i);// 10
如果将 var 替换成 let ,那么就会报“ ReferenceError: i is not defined ”,因为循环结束后,i 这个变量就被回收了
如果上面的两个例子都不足以说服你,那么下面的这个例子会更有说服力。
var a = [];
for (var i = 0; i < 10; i++) {
a[i] = function () {
console.log(i);
}
}
a[6]();// 10
同样,把 var 替换成 let 那么,a[6]() 的结果就是 6 ,原因是使用var声明的变量是没有块级作用域的,循环变量和循环内部的i是同一个i,处于一个作用域,循环外变化,里面的也会同时改变。而 let 就不一样,i 只在本轮循环有用,循环后就被回收,下一次是重新定义的 i ,该轮循环的 i 是多少,循环里面的 i 就是多少。
注意:let 方式定义的变量是不存在变量提升的情况的。而 var 定义的变量存在变量提升,可以称之为 预解析。
// var 的情况:先解析声明,在执行语句
console.log(foo ); // 输出undefined
var foo = 2 ;
// 等同于
var foo;
console.log(foo);
foo = 2;
// let 的情况
console.log(bar); // 报错ReferenceError,还未声明
let bar = 2 ;
总结来说,使用 let 声明的变量会绑定这个区块,也就是说,只要在声明 let 的变量之前使用这个变量,就会报错,有个术语可以描述在 let 之前的区域,称之为“暂定性死区”(temporal dead zone,简称 TDZ)
if (true) {
// TDZ开始
tmp = 'abc'; // ReferenceError
console.log(tmp); // ReferenceError
let tmp; // TDZ结束
console.log(tmp); // undefined
tmp = 123;
console.log(tmp); // 123
}
除此之外,let不允许在相同作用域内,重复声明同一个变量,这样会报错。
- const:const声明一个只读的常量。一旦声明,常量的值就不能改变,所以必须一声明就必须初始化。它和 let 一样,具有块级作用域,不存在变量提升,存在“暂定性死区”,不允许重复声明的特点。不同的是,const 在声明的同时必须赋值,不然会报错。
const的本质:const 保证的是变量指向的那个内存地址所保存的数据不得改动。对于基本类型来说,值就保寸在变量指向的那个内存地址中,无法再更改。对于复杂类型来说,变量里存储的并不是真正的对象,而是指向这个对象内存地址的指针,const 保证就是这个指针一直指向该对象而已,无法保证这个对象的数据结构是否发生改变。
const a = {};
a.name = "laocao";
console.log(a.name);// "laocao"
a = {};
console.log(a.name);// TypeError: Assignment to constant variable.
不过有一种办法可以保证,对象的数据结构无法被更改,那就是 Object.freeze(obj),obj 就是要冻结的对象。详细内容可以参考 MDN
- import:引入模块变量
- class:定义类名
变量的类型有哪些
- 基本类型:Number,String,Boolean,Null,Undefined,Symbol(es6)
- 引用类型:Object
如何判断变量的类型
-
基本类型的判断:typeof
原理:识别变量机器码的低位1-3位来判断该变量是什么类型。
– 000:对象
– 010:浮点数
– 100:字符串
– 110:布尔值
– 1:整数
而 null 的机器码是全 0 ,undefined 用 −2^30 整数来表示,所以判断 null 这个原始值类型的时候就被识别成对象。 -
引用类型的判断:instanceof
原理:主要是判断左边的对象是不是右边对象的实例对象,这里其实就是 return obj1.proto === obj2.prototype 的布尔值,缺点就是无法判断具体的对象。这里涉及到原型的知识,具体可以看一下我写的文章 -
通用方法:Object.prototype.toString.call()
作用域
作用域其实就是变量和函数所在的执行环境
三种作用域
- 局部作用域:一般是指函数执行的环境,函数外无法访问函数里面的变量,除非是没有声明的变量
- 全局作用域:无论函数内还是函数外都可以访问的变量的环境(这里涉及到顶级对象与全局变量的歧义)
- 块级作用域:es5是没有块级作用域的,但是可以模拟块级作用域。
(1):使用es6中 let 和 const 关键字
(2):使用IIFE(立即调用函数表达式)
作用域链
当代码在一个环境中执行时,会创建变量对象的一个作用域链(scope chain)。作用域链的用途,是保证对执行环境有权访问的所有变量和函数的有序访问。标识符解析是沿着作用域链一级一级地搜索标识符的过程。搜索过程始终从作用域链的前端开始,然后逐级地向后回溯,直至找到标识符为止(如果找不到标识符,通常会导致错误发生)。
也就是说,你在函数中使用了某个变量,那么js引擎就会去找这个变量的值,从当前环境到一直到全局环境,找到就停止,没找到就报错。
var a = 10;
function fn1 () {
var b = 5;
function fn2 () {
var c = a + b;
return c;
// 这层作用域可以访问a,b,c三个变量
}
fn2();
//这层只能访问a,b
}
fn1();
// 这层只能访问a
内存问题
JavaScript 具有自动垃圾收集机制,也就是说,执行环境会负责管理代码执行过程中使用的内存。
堆和栈
栈:是一种线性的数据结构,自动分配固定大小的内存,在js中主要存储基本类型的值以及指向引用类型地址的指针,由系统自动释放,具有先进后出的特点
堆:是一种杂乱无章的数据结构,没有固定大小,在js中主要存储引用类型的数据
浅拷贝和深拷贝
浅拷贝就是复制一个指针地址,然后指向同一块堆空间,那么不管哪个指针发生变化,堆中存储的对象都会跟着变化。
var obj1 = {
age:12,
car:["奥迪", "奔驰", "特斯拉"],
};
var obj2 = obj1;
console.log(obj1);
console.log(obj2);
深拷贝:把一个对象中所有的属性或者方法,一个一个的找到,并且在另一个对象中开辟相应的空间,一个一个的存储到另一个对象中,深拷贝效率极其低下,很少使用。
const obj1 = {
age:10,
name:"老曹",
car:["奔驰","宝马"],
books:{
size:"4k",
pages:300
}
};
const obj2 = {};
//定义一个函数将对象a的属性拷贝到b
function copy(obj1, obj2) {
//遍历obj1所有属性
for (let key in obj1) {
//保存每一个属性
let item = obj1[key];
//判断属性是什么类型,如果是对象或者数组类型则将里面的属性一个一个拷贝,使用递归
if (item instanceof Array) {
//如果obj1某属性的类型为数组,则在我obj2开辟一个空间存储该数组
obj2[key] = [];
copy(item, obj2[key]);
} else if (item instanceof Object) {
//如果obj1某属性的类型为对象,则在obj2开辟一个空间存储该对象
obj2[key] = {};
copy(item, obj2[key]);
} else {
//如果是基本类型的值,则直接赋值
obj2[key] = item;
}
}
}
copy(obj1, obj2);
console.log(obj1,obj2);
obj2.age = 20;
console.log(obj1,obj2);
垃圾回收机制(了解)
- 标记清除(常见)
这是js中最常用的垃圾收集方式。我的理解是在一个环境中如函数声明了一个变量,函数执行完毕后,该变量就会被标记,在一定的周期内回收掉该变量所占用的空间。不同浏览器垃圾收集的间隔是不一样的。
- 引用计数
引用计数的含义是跟踪记录每个值被引用的次数。当声明了一个变量并将一个引用类型值赋给该变量时,则这个值的引用次数就是1。如果同一个值又被赋给另一个变量,则该值的引用次数加1。相反,如果包含对这个值引用的变量又取得了另外一个值,则这个值的引用次数减1。当这个值的引用次数变成0 时,则说明没有办法访问这个值了,因而就可以将其占用的内存空间回收回来。
引用计数存在一定的性能问题,比如两个相互引用的变量存在循环引用,就无法被回收,此时需要手动设置为 null 才能断开循环。这种问题主要存在于IE浏览器,其BOM 和DOM 中的对象就是
使用C++以COM(Component Object Model,组件对象模型)对象的形式实现的,而COM 对象的垃圾收集机制采用的就是引用计数策略
参考文献
- 【1】阮一峰es6入门
- 【2】MDN在线文档
- 【3】浅谈 instanceof 和 typeof 的实现原理
- 【4】《JavaScript高级程序设计》(第三版)