5.有关堆栈内存、函数的底层处理机制、作用域、闭包及闭包的应用

147 阅读10分钟

1.栈内存Stack & 堆内存Heap

  • declare & defined
  • ECStack执行环境栈(Execution Context Stack)和 EC(Execution Context)
  • GO(Global Object)
  • VO(Varible Object)
var a = 12;
var b = a;
b = 13;
console.log(a);

基本类型:按值操作,所以也叫值类型
引用类型:操作的是堆内存的地址(按引用地址操作的)

image.png

2.有关堆栈内存的题

2.1 创建应用数据类型值得步骤及使用

var a = {
	n: 12
};
var b = a;
b['n'] = 13;
console.log(a.n);

image.png

2.2

image.png

2.3

image.png

image.png

3.函数的底层处理机制及作用域和作用域链

image.png

4.浏览器垃圾回收机制GC(Garbage Collection)

image.png

5.闭包

let x = 5;
function fn(x){
  return function(y){
    console.log(y + (++x);
  }
}
let f = fn(6);
f(7);
fn(8)(9);
f(10);
console.log(x);

image.png

//扩展:去掉fn中的形参x
let x = 5;
function fn(){
  return function(y){
    console.log(y + (++x);
  }
}
let f = fn(6);
f(7);
fn(8)(9);
f(10);
console.log(x);

5.1 闭包练习题1

let a = 0,
    b = 0;
function A(a){
  A = function(b){
    alert(a + b++);
  };
  alert(a++);
}
A(1);
A(2);

image.png

5.2 闭包练习题2

/**
 * EC(G)
 *  变量提升:
 *      var a;
 *      var b;
 *      var c;
 *      test = 0x000; [[scope]]:EC(G)
 * 
 */
var a = 10,
    b = 11,
    c = 12;
function test(a){
    /**
     * EC(TEST)
     *  作用域链:<EC(TEST),EC(G)>
     *  形参赋值:a = 10;
     *  变量提升:
     *      var b;
     */
    a = 1;
    var b = 2;
    c = 3;
}
test(10);
console.log(a,b,c);// 10 11 3

5.3 闭包练习题3

/**
 * EC(G)
 *  变量提升:
 *      var a;
 *      b = 0x000; [[scope]]:EC(G)
 *  代码执行
 */
var a = 4;
function b(x,y,a){
    /**
     * EC(B)
     *  作用域链:<EC(B),EC(G)>
     *  初始化this:window
     *  初始化arguments(实参集合,类数组):
     *      {
     *          0:1,
     *          1:2,//=>10 此时a的值也会跟着改为10
     *          2:3,
     *          length:3,
     *          callee:function b(){...}
     *      }
     *      =>在JS的非严格模式下,当“初始化arguments”和“形参赋值”完成后,会给两者建立一个“映射”机制:集合中的每一项和对应的形参变量绑定在一起了,一个修改都会跟着更改!而且只会发生在“代码执行之前”建立这个机制!
     *      =>在JS的严格模式下,没有映射机制,也没有arguments.callee这个属性;箭头函数中没有arguments;
     *  形参赋值:x = 10 y=2 a=3
     *  变量提升:--
     */
    console.log(a);//3
    arguments[2] = 10;
    console.log(a);//10
}
a = b(1,2,3);//a=undefined 因为函数没有返回值
console.log(a);// undefined

5.4 闭包练习题4

// 递归
function fn(){
    // 存储的是当前函数本身:很多时候这样操作方便递归调用
    console.log(arguments.callee);
}
-----------
let obj = {
    name: 'AA',
    fn: function (n){
        n++;
        if (n > 12) return;
        console.log(n);
        //obj.fn(n); this->obj
        arguments.callee(n); // this->arguments  相当于重新递归fn
    }
};
obj.fn(10);

(function (i){
    if(i >= 3) return;
    console.log(i);
    arguments.callee(++i);
})(i);
----------------------
/* 
JS 严格模式下,不支持arguments.callee,此时我们该如何让匿名函数实现递归呢? 
=>匿名函数具名化:这个一个非常规范的操作,就是给匿名函数设置一个名字
	+ 此时这个名字可以在当前函数形参的私有上下文中使用,代表单签函数本身
	+ 此名字不能在外部上下文中使用
	+ 在本函数的上下文中使用,他的值是不允许修改的
	+ 如果当前的名字被上下文中的其他变量声明过,他的值是可以改动的,则名字是私有变量和具名化的函数没有任何关系了
*/

"use strict"
(function b (i){
    if(i >= 3) return;
    console.log(i);
    b(++i);
})(i);
console.log(b); //Uncaught ReferenceError: b is not defined 在函数外面访问不到
----------
(function b (){
    console.log(b); //-> f b() {...}
    b = 100; //他的值是不允许修改的
    console.log(b); //-> f b() {...}
})();
------------
(function b (){
    console.log(b); //-> undefined
    var b = 100; //
    console.log(b); //-> 100
})();
----------------
var b = 10;
(function b(){
  // 因为匿名函数具名化之后,此时上下文中的b都是匿名函数本身(其修改值无效)
  b = 20;
  console.log(b); //-> f b() {...}
})();
console.log(b);//10
-----------------------------------------
function fn(x,y,z){
    // 初始化arguments: [10,20] -> 它是类数组,我现在只是写成这种方便看的格式了
    // 形参赋值:x=10 y=20 z=undefined
    // z没有和arguments中任何一项产生映射机制
    x = 100;
    console.log(arguments[0]);//100

    arguments[1] = 200;
    console.log(y);//200

    z = 300;//代码运行后修改的z,不产生映射
    console.log(arguments[2]);//undefined
}
fn(10,20);

5.5 闭包练习题5

image.png

5.6 闭包练习题6

/**
 * EC(G)
 *  变量提升:
 *      var test;
 */
var test = (function(i){
    /**
     * 自执行函数执行 EC(ANY)
     *  作用域链:<EC(ANY),EC(G)>
     *  形参赋值:i=2
     *  变量提升:--
     *  代码执行
     */
    return function (){
       /**
        * EC(TEST)
        *  作用域链:<EC(TEST),EC(ANY)> 
        *  初始arguments:{0:5,length:1...}
        *  形参赋值:--
        *  变量提升:--
        *  代码执行
        */
        alert(i *= 2); // i = i*2 -> i是EC(ANY)闭包中的 -> '4'
    } //=>return 0x001; [[scope]]:EC(ANY)
})(2);
// test = 0x001
test(5);

5.7 闭包练习题7

image.png

6.关于闭包套娃的面试题

image.png

7.let、const和var的区别

let和var的区别?
+ 变量提升
+ 重复声明
+ 暂时性死区
+ 和GO的关系
- JS中声明变量
  + 传统:var function
  + ES6let const import
  
- let VS const
  + letconst声明的都是“变量”,具体的值是常量!
  + let声明一个变量,变量存储可以改值
  + const声明的变量,一旦赋值,则不能再和其他的值关联(不允许指针重新指向)
  + 基于const声明变量,必须设置初始值

- 变量提升:在当前上下文,代码执行之前,会把所有带“var/function”关键字的进行提前的声明或者定义
  + 带“var”的只是提前声明
  + 带“function”的是声明+定义

- 带var和不带var的区别
  + 带var 的时候就是声明变量,不带var的时候,没有变量提升
  + 在全局作用域下,带var 还是不带var 都是给GO添加了一个属性(也相当于给window),属性名就是此变量,属性值就是变量值

- let VS var
  + var存在变量提升,let不存在
  + 全局上下文中,基于var声明的变量,也相当于给GO(全局对象window)新增一个属性,并且任何一个发生值的改变,另外一个也会跟着变化(映射机制); 但是基于let声明的变量,就是全局变量VO(G),和GO没有任何的关系;
  + 在相同的上下文中,let不允许重复声明(不论你之前基于何种方式声明,只要声明过,则都不能基于let重复声明了);而var很松散,重复声明也无所谓,反正浏览器也只按照声明一次处理
    + 在代码之前,浏览器会自己处理很多事情:词法分析、变量提升
    + 在词法分析阶段,如果发现有基于let/const并且重复声明变量操作则直接报语法错误,整个代码都不会做任何的执行
  + 暂时性死区(浏览器暂存的BUG)
    + console.log(n); //Uncaught ReferenceError: n is not defined
    + console.log(typeof n); // undefined 基于typeof金策一个未被声明的变量,不会报错,结果是undefined
    + let不允许在代码执行之前使用变量
  + let/const/function会产生块级私有上下文,而var是不会的
    上下文&作用域
      + 全局上下文
      + 函数执行形成的“私有上下文”
      + 块级作用域(块级私有上下文)
        + 处了 对象/函数...的大括号之外(例如:判断体、循环体、代码块...)都可能会产生块级上下文

8.闭包应用

  • JS高阶编程技巧(本质:“闭包”的机制完成的)
  • 闭包的特点是:保护 和 保存
  • 弊端:占用内存,消耗浏览器的性能,闭包可以用,但是不能滥用

8.1 闭包进阶应用1:循环处理

全局变量污染问题

// setTimeout([function],[interval]):设置一个定时器,等待[interval]
/* 
  i是全局变量
  i=0 第一轮循环 
  setTimeout(() => {// 设置定时器的时候,这个函数是创建不是执行
    console.log(i);
  }, 1000)

  i=1 第二轮循环
    setTimeout(() => { 
        console.log(i);
    }, 2000);  
  i=2 第三轮循环
    setTimeout(() => { 
        console.log(i);
    }, 3000);

  i = 3 循环结束
  ------1000ms时间到了
  执行函数 ()=>{ console.log(i); }
    + 在形成的私有上下文中遇到变量i,发现并不是自己私有的,找上级上下文(全局)下的 i
    + 结果都是循环结束后的
*/
for (var i = 0; i < 3; i++) {
  setTimeout(() => {
    console.log(i); // 3 3 3
  }, (i + 1) * 1000)
}

解法一:自执行函数

for (var i = 0; i < 3; i++) {
  // 每一轮循环,自执行函数执行,都会产生一个私有的上下文:并且是把当前这一轮循环,全局变量i的值作为实参,传递给私有上下文中的形参i
  /**
   *  EC(AN1) 形参赋值:i=0
   *  EC(AN2) 形参赋值:i=1
   *  EC(AN3) 形参赋值:i=2
   */
  //=>每一个形成的私有上下文,都会创建一个“箭头函数堆”,并且把其赋值给 window.setTimeout,这样等价于,当前上下文中的某些内容,被上下文以外的东西给占用了,形成的上下文不会释放(私有变量i的值也不会被释放)  =>闭包
  (function (i) {
      setTimeout(() => {
          console.log(i); //1,2,3
      }, (i + 1) * 1000);
  })(i);
}

解法二:proxy返回执行的函数

// let xxx = proxy(0) -> proxy执行会产生闭包,闭包中私有的形参变量存储传递的实参信息
const proxy = i => {
    return () => {
        console.log(i); // 1,2,3
    }
};
for (var i = 0; i < 3; i++) {
    setTimeout(proxy(i), (i + 1) * 1000);
    // 到达时间后,执行的是Proxy返回的小函数
}

解法三:let块级上下文

for (let i = 0; i < 3; i++) {
    // 每一轮循环都会产生一个私有的块级上下文,如果上下文中没有什么东西被外面占用,则本轮循环结束,私有块级上下文也会被释放掉;但是一旦有东西被占用,则会产生闭包,性能上会有所消耗
    setTimeout(()=>{
        // ...处理很多事情,但是函数中不需要使用i的值
    }, (i + 1) * 1000));
} 

案例:解决点击按钮显示索引的几种实现方法

  • 案例
<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>

<body>
  <button>1</button>
  <button>2</button>
  <button>3</button>

  <script>
    // NodeList类数组集合
    var buttons = document.querySelectorAll('button');

    for (var i = 0; i < buttons.length; i++) {
      buttons[i].onclick = function () {
        //每个都是3,因为循环完全局变量已经是3了
        // 当前点击按钮的索引:3
        console.log(`当前点击按钮的索引:${i}`);
      }
    }
  </script>
</body>

</html>

image.png

  • 方法一:闭包
// 第一种写法
var buttons = document.querySelectorAll('button');

for( var i = 0; i< buttons.length; i++){
  // 每一轮循环都会形成一个闭包,存储一个私有变量 i 的值(当前循环传递的i的值)
  //	+ 自执行函数执行,产生一个上下文EC(A)  私有形参变量i= 0/1/2
  //	+ EC(A)上下文中创建一个小函数,并且让全局buttons中的某一项占用创建的函数
  (function (i){
    buttons[i].onclick = function (){
      console.log(`当前点击按钮的索引:${i}`);
    };
  })(i);
}

//第二种写法
var buttons = document.querySelectorAll('button');
for(var i = 0; i < buttons.length; i++){
  buttons[i].onclick = (function(i){
    return function(){
      console.log(`当前点击按钮的索引:${i}`);
    };
  })(i);
}

//第三种写法let
var buttons = document.querySelectorAll('button'); 
for( let i = 0; i< buttons.length; i++){
  buttons[i].onclick = function (){
    console.log(`当前点击按钮的索引:${i}`);
}

image.png

  • 方法二:自定义属性
// 性能强于闭包
var buttons = document.querySelectorAll('button'); 
for( var i = 0; i< buttons.length; i++){
  // 每一轮循环都给单签按钮(对象)设置一个自定义属性:存储他的索引
  buttons[i].myIndex = i;
  buttons[i].onclick = function (){
    // this => 当前点击的按钮
    console.log(`当前点击按钮的索引:${this.myIndex}`);
}
  • 方法三:事件委托
//在结构上设定自定义属性index,存储按钮的索引
<button index='0'>1</button>
<button index='1'>2</button>
<button index='2'>3</button>

// 方案三:比之前的性能提高40%-60%
// + 不论点击body中的谁,都会触发body的点击事件
// + ev。target是事件源:具体点击的是谁
document.body.onclick = function(ev){
  var target = ev.target,
      targetTag = target.tagName;
  
  // 点击的是button按钮
  if(targetTag === "BUTTON"){
    var index = target.getAttribute('index');
    console.log(`当前点击按钮的索引:${index}`);
  }
}

8.2 闭包进阶应用2:基于闭包实现早期的模块化思想

单例设计模式

  • 在没有对象类型值之前,我们很容易产生“全局变量的污染”
  • 解决变量冲突:闭包机制 -> 保护
  • 把描述同一个事物的属性,存放到相同的命名空间(对象)下,以此来进行分组,减少全局变量的污染;每一个对象都是Object这个类的不同的实例,这种设计模式叫做“单例设计模式”
let utils = (function () {
    let iswindow = true,
        num = 0;
    const queryElement = function queryElement(selector) {};
    const formatTime = function formatTime(tiem) {};

    // 把需要供外界调用的方法,存储到一个命名空间(对象)中
    return {
        queryElement,
        formatTime
    };
})();

8.3 闭包进阶应用3:惰性函数

/* 
获取元素的样式
   + window.getComputeStyle([元素对象]) 不兼容IE6-8
   + [元素对象].currentStyle
属性名 in 对象:检测当前这个属性是否属于这个对象

性能上的问题:
在某个浏览器中渲染页面(渲染代码)
    + 第一次执行get_css 需要验证浏览器的兼容性
    + 后期每一次执行 get_css ,浏览器的兼容性检测都会执行一遍 “这个操作时没有必要的”
*/
function get_css(element, attr) {
  if (window.getComputedStyle) {
    return window.getComputedStyle(element)[attr];
  }
  return element.currentStyle[attr];
}

// 基于“惰性函数”提高上述的性能“
function get_css(element, attr) {
  if (window.getComputedStyle) {
    // 第一次执行get_css,根据浏览器的兼容情况,对外部的get_css函数进行重构
    get_css = function (element, attr) {
      return window.getComputedStyle(element)[attr];
    }
  } else {
    get_css = function (element, attr) {
      return element.currentStyle[attr];
    }
  }
  // 第一次执行也是需要获取到结果的,所以我们把重构的函数执行一次
  return get_css(element, attr);
}
var w = get_css(documnet.body, 'width');
console.log(w);

//后续再次执行get_css,执行是第一次重构后的小方法,,无需再次校验兼容性
var h = get_css(documnet.body, 'height');
console.log(w);

8.4 闭包进阶应用4:柯里化函数&重写reduce

  • 执行函数,形成一个闭包,把一些信息(私有变量和值)存储起来(保存作用)
  • 以后其下级上下文中如果需要用到这些值,直接基于作用域链查找机制,拿来直接用即可

实参依次相加

function fn(){
  // 执行fn传递的实参数信息
  let outer_args = Array.from(arguments);

  return function anonymous(){
      // 存储执行小函数传递的实参信息
      let innerArgs =Array.from(arguments);

      //存储量词执行函数传递的实参信息
      let params = outer_args.concat(innerArgs);
      // 数组相加 forEACH循环相加; join分隔,eval变为函数表达式
      return result =  params.reduce(function(result,item){
          return result + item;
      });
  };
}

/* 
const fn = (...outer_args) =>{
  return (...innerArgs)=>{
      return outer_args.concat(innerArgs).reduce((result,item)=>{
          return result + item;
      });
  }
} 

const fn = (...outer_args) =>(...innerArgs)=>outer_args.concat(innerArgs).reduce((result,item)=> result + item);
*/

let res = fn(1,2)(3);
console.log(res); //=>6 1+2+3

重写reduce