一、js的运行机制及原理:

1. js运行的环境(栈内存):作用域

作用域是 js 运行的环境,另外一个功能是保存基本数据类型。在 js 中作用域分为:

  • 全局作用域:当页面打开时,首先形成一个全局作用域,执行全局中的代码,全局作用域是 window;
  • 私有作用域(函数作用域)当函数执行时,会形成一个函数作用域,这个作用域用来保存函数中的基本数据类型同时执行函数代码;
  • 块级作用域(类似私有作用域 ES6)

2. js运行过程

  • 在 js 代码执行前,浏览器会开辟一个全局作用域,然后执行变量提升,完成变量提升操作后,代码开始从上到下开始执行;
  • 当执行时,如果遇到基本数据类型,就在作用域中存储该基本数据类型;
  • 如果遇到引用数据类型,则浏览器会再次分配一个堆内存,然后把引用数据类型的内容存储到堆内存中,接下来再把这个堆内存的地址赋值给变量(此时这个地址是存储在作用域内存中的);
  • 遇到函数执行时,会经历以下几步:
    1. 浏览器开辟一个私有作用域;
    2. 形参赋值,把执行时的实参赋值给函数形参变量;
    3. 私有作用域中变量提升
    4. 函数代码从上到下执行

3. 私有变量和全局变量

  • 全局变量:在全局作用域中声明的变量
  • 私有变量:函数的形参以及在函数私有作用域中声明的变量

4. 预解释只发生在当前作用域,如果函数不执行,函数中的变量不会进行变量提升。

二、作用域链

  1. // 作用域(scope):js代码执行的执行的环境,另外还可以存储基本数据类型的值。
  2. // 全局作用域:打开一个页面的时候,浏览器就会创建一个全局作用域(window 对象)
  3. // 私有作用域(函数作用域):函数执行的时候浏览器开辟的,只要函数执行就会新开辟一个私有作用域;
  4. // 块级作用域:把代码块(if / for 循环...的代码块变成作用域),功能和私有作用域类似,是 ES6 新增的;
  5. var num = 100;
  6. function sum(a, b) {
  7. return a + b;
  8. }
  9. sum();
  10. sum();
  11. function fn(a) {
  12. var x = 6;
  13. // var num = 15;
  14. return num + a; // ?? fn 执行的时候为什么拿到外面的变量 num 的值呢?
  15. }
  16. var r = fn(3);
  17. console.log(r); // 103 ?
  18. // console.log(x); // ?? 为啥我拿不到私有变量呢?

作用域链:变量的查找机制;当我们引用一个变量时,浏览器会首先在当前作用域中查找,看当前作用域中是否有这样一个变量,如果有直接使用。如果没有,就向上一级作用域查找,如果找到就使用,如果没找到,就继续向上查找,一直找到 window,如果还没有,如果是引用就会报错,如果是赋值就是给 window 增加一个同名属性;


  • 形参也是私有属性

作用域链的查找机制.png

三、栈内存不销毁

  1. /*
  2. * 目标:
  3. * 1. 理解函数栈内存不销毁的情形
  4. * 2. 理解函数栈内存不销毁时,保存在栈内存中的数据也不销毁
  5. * */
  6. // 栈内存不销毁的特殊情况:一般情况下,函数执行完,栈内存就会销毁。但是函数执行时,函数作用域中有一些值被其他地方占用,此时作用域不能销毁(栈内存不销毁);
  7. function add() {
  8. var n = 1; // 2 3
  9. return {
  10. name: '珠峰',
  11. n: 2,
  12. addNum: function () {
  13. n++; // 这个 n 是 add 函数作用域中的私有变量 n;对象的花括号不是作用域;
  14. console.log(n);
  15. }
  16. }
  17. }
  18. var obj = add(); // add() 执行后返回的对象被 obj 所接收,所以 add 执行的作用域不能被销毁,所以栈内存不能释放,保存在作用域中的变量n的值也不会被销毁。
  19. obj.addNum(); // 2
  20. obj.addNum(); // 3
  21. add(); // 这样执行 add 函数,没有人接收它的返回值,执行完后,add 的作用域也就销毁了。
  22. function foo() {
  23. var n = 1;
  24. n++;
  25. console.log(n);
  26. }
  27. foo(); // 2
  28. foo(); // 2
  29. // 1. 当函数执行完,如果函数返回了一个引用数据类型的值,并且被外面的变量或者属性接收,此时函数执行的作用域不能销毁,在这个作用域中储存的值也不会销毁;
  30. function f1() {
  31. console.log('1');
  32. }
  33. f1();
  34. f1();
  35. // 2. 函数内部的引用数据类型值,被函数外的变量或者属性占用,此时函数作用域不能销毁;
  36. var x = null;
  37. function happyPass() {
  38. x = {
  39. name: 'zf'
  40. }
  41. }
  42. happyPass(); // 因为全局变量 x 占用着 happyPass 里面的一个对象,所以 happyPass 的作用域不能销毁
  43. function foo1() {
  44. var n = 15;
  45. return function (x) {
  46. // 形参赋值:x = 2 // 3 18
  47. // 变量提升:无
  48. x++;
  49. x += n;
  50. console.log(x); // 18
  51. console.log(n); // 15
  52. }
  53. }
  54. var f2 = foo1();
  55. f2(2);
  56. function f4() {
  57. return function () {
  58. console.log('赵四');
  59. }
  60. }
  61. var obj2 = {
  62. getF4: f4()
  63. };
  64. // obj2 的 getF4 这个属性占用着 f4 的返回值,所以 f4 执行时,作用域不销毁。

堆栈内存释放案例.png