变量
原始值和引用值
ESMAScript有两种类型的数据:原始值和引用值。
- 原始值:Undefined、Null、Boolean、Number、String和Symbol;
- 引用值:由多个值构成的对象;
下面详细分析一下他们的不同之处。
原始值和引用值的区别
1.动态属性
引用值可以随时增加、修改和删除其属性和方法;
js
let person = new Object()
person.name = "张三"
console.log(person.name) // 张三
原始值不能有属性,虽然给原始值增加属性不会报错,但是获取不到该属性;
js
let person = "张三"
person.age = 30
console.log(person.age) // undefined
2. 复制值
原始值复制:
js
let num1 = 5
let num2 = num1
引用值复制:
js
let obj1 = new Object()
let obj2 = obj1
obj1.name = "张三"
console.log(obj2.name) // 张三
3.传递参数
ECMAScript中所有函数的参数都是按值传递的,就像从一个变量复制到另一个变量一样。变量有按值和按引用访问,而传参只有按值传递。
如果参数是原始值会好理解一些,函数内部的num加10,不会影响函数外部的count变量。num和count是不会互相影响的。
js
function addTen(num) {
num += 10;
return num;
}
let count = 20;
let result = addTen(count);
console.log(count); // 20
console.log(result); // 30
如果参数是引用值,person和obj都指向同一个对象。看起来好像参数是按引用传递的,那我们再看一个例子;
js
function setName(obj){
obj.name = "张三"
}
let person = new Object()
setName(person)
console.log(person.name) // 张三
如果参数是按引用传递的,person应该改为李四
啊,但我们访问person.name,它还是张三
。当函数中的参数改变时,原始的引用依然没有变。在obj被重写成新的对象,这个新的对象在函数执行完成时就销毁了。
js
function setName(obj){
obj.name = "张三"
obj = new Object()
obj.name = "李四"
}
let person = new Object()
setName(person)
console.log(person.name) // 张三
4.确定类型typeof
typepf能判断出String、Number、Boolean、undefined;如果是object或是null,它就会都返回Object
。typeof对原始值很有用,但它对引用值用处不大。
如果变量是引用类型,可以用instanceof判断,它会返回true。
执行上下文和作用域
执行上下文和作用域是什么?
每个函数都有自己的上下文;当代码执行进入函数时,函数的上下文被推到上下文栈上;函数执行完之后,上下文栈会弹出该函数的上下文,将控制权返还给之前的执行上下文。
js
var color = "blue";
function changeColor() {
let anotherColor = "red";
function swapColors() {
let tempColor = anotherColor;
anotherColor = color;
color = tempColor;
// 这里可以访问color、anotherColor和tempColor
}
// 这里可以访问color和anotherColor,但访问不到tempColor
swapColors();
}
// 这里只能访问color
changeColor();
- 上面的🌰涉及到3个上下文:全局上下文,changeColor()的局部上下文和swapColors()的局部上下文。
全局上下文
有一个变量color和函数changeColor()。changeColor()
的局部上下文中有一个变量anotherColor和swapColors(),但在这里可以访问全局上下文中的变量color。swapColors()
的局部上下文中有一个变量tempColor,只能在当前这个上下文中被访问,全局上下文和changeColor()局部上下文中都无法访问到变量tempColor。- 在swapColors()中可以访问另外两个上下文中的变量。
上图展示了这个🌰的作用域链。内部上下文能通过作用域链访问外部上下文,但外部上下文无法访问到内部上下文中的任何东西。
变量声明
var
先来看一个🌰,在函数中使用var声明变量,它会被添加到函数的局部上下文中。函数add()定义了一个局部变量sum,这个值作为函数的值被返回,但是变量sum在函数外部是访问不到的。
js
function add(num1, num2) {
var sum = num1 + num2;
return sum;
}
let result = add(10, 20);
console.log(sum); // 报错:sum在这里不是有效变量
我们把var去掉呢。
js
function add(num1, num2) {
sum = num1 + num2;
return sum;
}
let result = add(10, 20);
console.log(sum); // 30
上面的🌰中,如果变量未经声明就被初始化了,它就会被自动添加到全局上下文中,因此sum在add()函数之外可以被访问了。
var声明会被放到函数或者全局作用域的顶部,这个现象叫做"提升"(Hoisting)
。
js
var name = "张三"
// 等价于
name = "张三"
var name
js
functin fn1(){
var name = "张三"
}
// 等价于
function fn2(){
var name
name = "张三"
}
如果我们在变量声明之前打印变量,会发生什么呢?变量提升之后会打印出undefined
。
js
console.log(name) // undefined
var name = "张三"
function fn1(){
console.log(name) // undefined
var name = "张三"
}
let
let和var很相似,但它的作用域是块级的。
js
if(true){
let a
}
console.log(a) // ReferenceError: a没有定义
while(true){
let b
}
console.log(b) // ReferenceError: b没有定义
function foo(){
let c
}
console.log(c) // ReferenceEffor: c没有定义
// 这里如果用var,也会报错
{
let d
}
console.log(d) // ReferenceEffor: d没有定义
let和var还有一个不同之处就是在同一作用域中let不能声明两次。重复的var声明会被忽略,而重复的let会抛出SyntaxError。
let在JavaScript运行时也会被提升,但是由于"暂时性死区"(temporal dead zone)的缘故,并不能在声明之前使用let变量。let的提升和var是不一样的。
const
const声明变量的时候就得初始化,我是说原始值类型,如果重新赋予新值就会报错。 但如果const声明了引用类型变量,对象的健是不受限制的。
js
const o1 = {}
o1 = {} // TypeError: 給常量赋值
const o2 = {}
o2.name = "张三"
console.log(o2.name) // 张三
标识符查找
js
var color = "blue";
function getColor() {
return color;
}
console.log(getColor()); // blue
调用函数getColor()时会引用变量color。为确定color的值会分两步搜索。第一步在函数getColor()内部的变量color;如果没找到就会去全局上下文中查找,因为全局有color,所以就找到了。
js
var color = "blue";
function getColor() {
let color = "red";
return color;
}
console.log(getColor()); // red
在上面这个🌰中,在函数getColor()中找到了color,搜索就停止了。
js
var color = "blue";
function getColor() {
let color = "red";
{
let color = "green";
return color;
}
}
console.log(getColor()); // green
调用getColor()时会引用变量color,于是在局部上下文中搜索color,结果找到了值为"green"的color;因为变量color找到了,所以停止搜索。块级作用域不会改变搜索的流程,但是可以改变词法层级;
垃圾回收
在C和C++中需要开发者自己跟踪内存使用,这对开发工作带来了麻烦。JavaScript通过自动内存管理实现内存分配和垃圾回收,为开发工作减轻了负担。垃圾回收会定期查找不再使用的变量,然后释放它们的内存空间。因此垃圾回收需要去标记未使用的变量,这里有两种主要的方式。
两种标记未使用变量方式
标记清理(常用)
当变量进入上下文,比如在函数内部声明一个变量,这个变量会被加上存在上下文的标记。只要上下文中的代码在运行,就不会释放它们的内存。当变量离开上下文,就会被加上离开上下文的标记。
js
var m = 0, n = 19 //把m, n , add()标记为进入环境
add(m, n) // 把a, b, c标记为进入环境
console.log(n); // 把a, b, c标记为离开环境,等待垃圾回收
function add(a, b){
a++
var c = a + b
return c
}
引用计数
引用计数:每个值都记录它被引用的次数。声明a变量并给它赋一个引用值,引用值为1;如果a变量又被赋值给另一个变量b,那么引用数➕1;如果a变量被其他值覆盖了,那么引用数➖1;当变量的引用数为0时,就说明没法再访问这个值了;因此可以被垃圾回收了;
js
let a = new Object(); // 此对象的引用计数为1(a 引用)
let b = a; // 此对象的引用计数为2(a, b引用)
a = null // 此对象的引用计数为1(b引用)
b = null // 此对象的引用计数为0(无引用)
... // GC回收此对象
很快又有一个严重问题:循环引用;当对象A有一个指针指向对象B,而对象B也引用了对象A。这时objectA和objectB的引用数都是2;当函数结束后还会存在,它们的引用数永远不会变成0;如果函数被多次调用,就会导致大量内存永远不会被释放。
js
function problem() {
let objectA = new Object();
let objectB = new Object();
objectA.someOtherObject = objectB;
objectB.anotherObject = objectA;
}
内存管理
1.通过const和let声明提升性能
let和const都是块作用域,相比于使用var,使用let和const能更早地让垃圾回收程序介入,尽早回收应该回收的内存。
2.隐藏类
a2.author = "张三"
这种操作的频率和隐藏类的大小,会对性能产生明显的影响。
js
function Article() {
this.title = "hidden class";
}
let a1 = new Article();
let a2 = new Article();
// a1和a2会共享相同的隐藏类
a2.author = "张三"
// 此时a1和a2会对应两个不同的隐藏类
delete a2.author
// 使用delete也会使a1和a2不再共享相同的隐藏类
因此要避免a2.author = "张三"
这种操作,尽量在constructor中一次性声明所有的属性像这样;也要避免delete a2.author
操作,把它改成a2.author = null
像这样。
js
function Article() {
this.title = "hidden class";
this.author = "张三";
}
let a1 = new Article();
let a2 = new Article("李四");
a2.author = null
3.内存泄漏
以下几种操作会有内存泄漏的问题:
1.意外声明全局变量
name前面没有任何关键字,此时会把name当作window的属性来创建(相当于window.name = "张三"
),window不被清理name就不会消失。
js
function setName(){
name = "张三"
}
2.定时器
只要定时器一直运行,回调函数中的name就会一直占用内存。
js
let name = "张三";
setInterval(() => {
console.log(name);
}, 1000);
3.使用闭包
outer中创建一个内部闭包,只要返回的函数存在就不能清理name。
js
let outer = function () {
let name = "张三";
return function () {
return name;
};
};
4.静态分配与对象池 (选修内容)
当你的应用程序被垃圾回收严重拖了后腿,可以利用它提升性能。大多数情况下,这都属于过早优化,因此不用考虑。 如果有很多对象被初始化,然后又一下子超出了作用域,那么浏览器就会采用更加激进的方式垃圾回收,这样当然会影响性能。来看一个例子:
js
function addVector(a, b) {
let resultant = new Vector();
resultant.x = a.x + b.x;
resultant.y = a.y + b.y;
return resultant;
}
上面这个例子,调用这个函数时,会在堆上创建一个新对象,然后修改它,最后再返回给调用者。假如addVector方法被频繁调用,这里的对象更新速度很快,从而会频繁地进行垃圾回收。那么有更好的解决方法吗?
js
function addVector(a, b, resultant) {
resultant.x = a.x + b.x;
resultant.y = a.y + b.y;
return resultant;
}
更好的解决方法就是不要动态创建矢量对象,让它使用一个已有的矢量对象。