js 堆栈

JavaScript是一门轻级的编程语言, 我之前也用过C++和Java, 相比起来JavaScript是一门年轻简约的编程语言, 但是我很看好这门语言, 我记得之前看过一个大牛说现在的前端开发是黎明前的黑暗, 在数年之内必定会清晰明朗起来. 自己深表赞同, JavaScript现在的确是有很多的缺陷, 相比较起来它的依赖库也不如java那般丰富, 但是它一个最大的优点(个人认为)就是它的轻量级, 你仅仅需要一个浏览器(或者Node环境, 但是Node其实也是基于Chrome的V8引擎), 他就能完成自己所有的工作, 我坚信随着各种标准的制定以及已经走在探索路上的前端开发师们能够很快为JavaScript带来它起飞的春天.

上面是我的个人希冀, 说到这篇文章, 主要是记录一下JavaScript的内存机制以及按值传递规则, 因为我在JavaScript的开发过程中会不由自主的把它和我也使用过的C++和Java进行比较, 我认为编程语言是互通的, 但是它们在某些细节上的处理有所不同, 则正是我们需要去注意的.

# 内存机制

在JavaScript中有两个存储概念, 是用来存储Object型数据的 ,是用来存储6种基本数据类型(分别是null, undefined, boolean, number, string和ES6中新引入的symbol), 对于我们平时使用的数组Array其实是Object的继承而已, 可以使用typeof运算符查看一个数据的类型, 例如typeof []就会输出object, 另外一个比较特别的就是函数类型, 函数类型的typeof输出的是function, 但是函数其实也是存储在中的, 而且可以认为是以字符串的形式存储的.

# 为什么有之分

与垃圾回收机制有关,为了使程序运行时占用的内存最小。 当一个方法执行时,每个方法都会建立自己的内存栈,在这个方法内定义的变量会逐个放入这块栈内存里,随着方法的执行结束,这个方法的内存栈也将自然销毁了。因此,所有在方法中定义的变量都是放在栈内存中的; 当我们在程序中创建一个对象时,这个对象将被保存到运行时数据区中,以便反复理由(因为对象的创建成本通常比较大),这个运行时数据区就是堆内存。堆内存中的对象不会随方法的结束而销毁,即使方法结束后,这个对象还可能被另一个引用变量所引用(方法的参数传递时很常见),则这个对象依然不会被销毁,只有当一个对象没有任何引用变量引用它时,系统的垃圾回收机制才会在核实的时候回收它。 --参考

在我们知道了JavaScript中的对象是如何存储的之后, 我们就要看一下当我们生成一个变量的时候到底发生了什么.

# 生成变量

假如我们生成的是一个存储基本数据类型的变量, 例如let a = 3或者let b = 'hello', 那么有如下两步:

  1. 中直接开辟出一小块空间
  2. 把你赋予的数据(3或者hello)存储到这个空间中

也就是说数据是直接存储在中, 但是当我们生成的是一个存储了对象类型的变量, 例如let c = {name: 'krics'}, 那么这个时候过程就要复杂一些:

  1. 中开辟一块空间
  2. 把你赋予的数据{name: 'krics'}存储到这个空间中
  3. 中开辟一小块空间
  4. 将之前存储了对象数据的空间的地址(指针形式)存储到现在刚刚开辟的这个空间中

所以我们真正的数据其实是存储在中的, 我们拿到的c变量里面只是存储了数据的真实地址, 当我们需要访问或者操作数据的时候, JavaScript就会根据这个地址去找到对应的数据, 然后访问或者操作它.

# 值的拷贝

我们需要永远记住最关键的一点: JavaScript中只存在按值传递!!! 不同于C++中或者Java中经常出现的指针操作, 在JavaScript中不会出现按引用传递, JavaScript永远只操作一个变量最直接的值, 并不会考虑这个值是基本数据类型还是一个指针, 因为如果是指针, 也并不会去按照指针找到具体的数据, 然后拷贝数据什么的, 是指针, 那我就传递这个指针的字面值, 简单粗暴明了.

例如:

let a = 'hello';
let b = a;

b = 'yell';

console.log(a); // => 'hello'
console.log(b); // => 'yell'

这里发生的故事是:

  1. 中开辟了一个空间叫a, 然后在a里面存入了一个字符串hello
  2. 中开辟了一个空间叫b, 然后在b里面存入了一个字符串hello(按值传递, 值是hello, 那么就再存一个hello)
  3. 修改b的值为yell
  4. 输出a的值, 没被改变过, 所以输出hello
  5. 输出b的值, 先是hello, 后来被改成了yell, 那么最后输出的就是yell

那么我们举一个对象的例子又如何呢?

let c = {name: 'krics'};
let d = c;

c.name = 'leo';
console.log(d.name); // => 'leo'

d.name = 'troy';
console.log(c.name); // => 'troy'

这里发生的故事是:

  1. 先在中开辟一块空间名字叫做c, 然后在中开辟一块空间, 存入数据{name: 'krics'}, 然后把中刚存储的数据的地址存到c
  2. 中开辟一块空间名字叫做d, 然后把c中存储的值也就是{name: 'krics'}的地址在d中再存储一份
  3. c指向的对象中的name的值改为字符串leo,
  4. dc指向的是同一个对象, 所以第三步中通过c改了name的值以后, 通过d访问这个name时得到的也是改变后的值leo
  5. 第五和第六步与第三和第四步做法类似

这里给出一个很有趣的思考题:

var a = {n:1};
var b = a;
a.x = a = {n:2};

console.log(a.x); // => 想想这里 a.x 的值是什么
console.log(b.x); // => 想想这里 b.x 的值是什么

这里给个提示, 上面主要涉及到三个细节点, 一是JavaScript中正常运算顺序为从右到左, 二是.点运算符的优先级高于=等号, 三就是我们之前讨论过的对象如何赋值问题, 答案可以参考luoqua的文章

最后我仍然要强调一点: JavaScript中只存在按值传递!!!(可以参考<JavaScript高级程序设计>一书中第四章'变量. 作用域和内存问题')