JavaScript深入进阶调用堆栈及作用域闭包

一、调用堆栈

1、JavaScript深入之内存空间思考

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

a.x 	// --> undefined
b.x 	// --> {n: 2}

答案已经写上面了,
这道题的关键在于
1、优先级。.的优先级高于=,所以先执行a.x,堆内存中的{n: 1}就会变成{n: 1, x: undefined},改变之后相应的b.x也变化了,因为指向的是同一个对象。
2、赋值操作是从右到左,所以先执行a = {n: 2},a的引用就被改变了,然后这个返回值又赋值给了a.x,需要注意的是这时候a.x是第一步中的{n: 1, x: undefined}那个对象,其实就是b.x,相当于b.x = {n: 2}
在这里插入图片描述
2、

var a = { name: '前端开发' }
var b = a;
b.name = '进阶';

// 这时a.name的值是多少

‘进阶’。对于问题2,a、b都是引用类型,栈内存中存放地址指向堆内存中的对象,引用类型的复制会为新的变量自动分配一个新的值保存在变量对象中,但只是引用类型的一个地址指针而已,实际指向的是同一个对象,所以修改b.name的值后,相应的a.name也就发生了改变。

3、 从内存来看 null 和 undefined 本质的区别是什么?

解答: 给一个全局变量赋值为null,相当于将这个变量的指针对象以及值清空,如果是给对象的属性 赋值为null,或者局部变量赋值为null,相当于给这个属性分配了一块空的内存,然后值为null, JS会回收全局变量为null的对象。 给一个全局变量赋值为undefined,相当于将这个对象的值清空,但是这个对象依旧存在,如果是给对象的属性赋值 为undefined,说明这个值为空值。

扩展下:
声明了一个变量,但未对其初始化时,这个变量的值就是undefined,它是 JavaScript 基本类型 之一。

var data;
console.log(data === undefined); //true

对于尚未声明过的变量,只能执行一项操作,即使用typeof操作符检测其数据类型,使用其他的操作都会报错。

 //data变量未定义
console.log(typeof data); // "undefined"
console.log(data === undefined); //报错

值 null 特指对象的值未设置,它是 JavaScript 基本类型 之一。 值 null 是一个字面量,它不像undefined 是全局对象的一个属性。null 是表示缺少的标识,指示变量未指向任何对象。 // foo不存在,它从来没有被定义过或者是初始化过:

foo;
"ReferenceError: foo is not defined"

// foo现在已经是知存在的,但是它没有类型或者是值:
var foo = null; 
console.log(foo);	// null

4、 ES6语法中的 const 声明一个只读的常量,那为什么下面可以修改const的值?

const foo = {}; 
foo = {}; // TypeError: "foo" is read-only
foo.prop = 123;
foo.prop // 123
// 将 foo 指向另一个对象,就会报错
foo = {}; // TypeError: "foo" is read-only

解答: const实际上保证的,并不是变量的值不得改动,而是变量指向的那个内存地址所保存的数据不得改动。对于简单类型的数据(数值、字符串、布尔值),值就保存在变量指向的那个内存地址,因此等同于常量。但对于复合类型的数据(主要是对象和数组),变量指向的内存地址,保存的只是一个指向实际数据的指针,const只能保证这个指针是固定的(即总是指向另一个固定的地址),至于它指向的数据结构是不是可变的,就完全不能控制了。因此,将一个对象声明为常量必须非常小心。
5、全局变量

<script>
    console.log(1,fun)
    console.log(2,person)
</script>
<script>
    console.log(3,person)
    console.log(4,fun)
    var person = "Eric";
    console.log(5,person)
    function fun() {
        console.log(6,person)
        var person = "Tom";
        console.log(7,person)
    }
    fun()
    console.log(8,person)
</script>

结果:
在这里插入图片描述

6、定时器

setInterval(function() {
    var node = document.getElementById('Node');
    if(node) {
        // 处理 node 和 someResource
        node.innerHTML = JSON.stringify(someResource));
    }
}, 1000);

7、对于闭包的定义:闭包是指有权访问另外一个函数作用域中的变量的函数
关键在于下面两点:

  • 是一个函数
  • 能访问另外一个函数作用域中的变量

二、作用域闭包

1、闭包可以访问当前函数以外的变量

function getOuter(){
  var date = '922';
  function getDate(str){
    console.log(str + date);  //访问外部的date
  }
  return getDate('日期是:'); //"今天是:922"
}
getOuter();

2、即使外部函数已经返回,闭包仍能访问外部函数定义的变量

function getOuter(){
  var date = '815';
  function getDate(str){
    console.log(str + date);  //访问外部的date
  }
  return getDate;     //外部函数返回
}
var today = getOuter();
today('今天是:');   //"今天是:815"
today('明天不是:');   //"明天不是:815"

3、闭包可以更新外部变量的值

function updateCount(){
  var count = 0;
  function getCount(val){
    count = val;
    console.log(count);
  }
  return getCount;     //外部函数返回
}
var count = updateCount();
count(815); //815
count(816); //816

4、作用域链
Javascript中有一个执行上下文(execution context)的概念,它定义了变量或函数有权访问的其它数据,决定了他们各自的行为。每个执行环境都有一个与之关联的变量对象,环境中定义的所有变量和函数都保存在这个对象中。
当访问一个变量时,解释器会首先在当前作用域查找标示符,如果没有找到,就去父作用域找,直到找到该变量的标示符或者不在父作用域中,这就是作用域链。
作用域链和原型继承查找时的区别:如果去查找一个普通对象的属性,但是在当前对象和其原型中都找不到时,会返回undefined;但查找的属性在作用域链中不存在的话就会抛出ReferenceError。
作用域链的顶端是全局对象,在全局环境中定义的变量就会绑定到全局对象中。
5、有嵌套的函数
当函数返回没有被引用的时候,就会被垃圾回收器回收。但是对于闭包,即使外部函数返回了,函数对象仍会引用它被创建时的作用域对象。

function createCounter(initial) {
  var counter = initial;
  
  function increment(value) {
    counter += value;
  }
  
  function get() {
    return counter;
  }
  
  return {
    increment: increment,
    get: get
  };
}

var myCounter = createCounter(100);
console.log(myCounter.get());   // 返回 100

myCounter.increment(5);
console.log(myCounter.get());   // 返回 105

6、作用域指的是一个变量和函数的作用范围,JS中函数内声明的所有变量在函数体内始终是可见的,在ES6前有全局作用域和局部作用域,但是没有块级作用域(catch只在其内部生效),局部变量的优先级高于全局变量。

变量提升

var scope="global";
function scopeTest(){
    console.log(scope);
    var scope="local"  
}
scopeTest(); //undefined

上面的代码输出是undefined,这是因为局部变量scope变量提升了,等效于下面

var scope="global";
function scopeTest(){
    var scope;
    console.log(scope);
    scope="local"  
}
scopeTest(); //undefined

注意,如果在局部作用域中忘记var,那么变量就被声明为全局变量。

7、作用域链

每个函数都有自己的执行上下文环境,当代码在这个环境中执行时,会创建变量对象的作用域链,作用域链是一个对象列表或对象链,它保证了变量对象的有序访问

作用域链的开始是当前代码执行环境的变量对象,常被称之为“活跃对象”(AO),变量的查找会从第一个链的对象开始,如果对象中包含变量属性,那么就停止查找,如果没有就会继续向上级作用域链查找,直到找到全局对象中。

function createClosure(){
    var name = "jack";
    return {
        setStr:function(){
            name = "rose";
        },
        getStr:function(){
            return name + ":hello";
        }
    }
}
var builder = new createClosure();
builder.setStr();
console.log(builder.getStr()); //rose:hello

上面在函数中返回了两个闭包,这两个闭包都维持着对外部作用域的引用。闭包中会将外部函数的自由对象添加到自己的作用域链中,所以可以通过内部函数访问外部函数的属性,这也是javascript模拟私有变量的一种方式。

  • 闭包试题1

由于作用域链机制的影响,闭包只能取得内部函数的最后一个值,这引起的一个副作用就是如果内部函数在一个循环中,那么变量的值始终为最后一个值。

var data = [];

for (var i = 0; i < 3; i++) {
  data[i] = function () {
    console.log(i);
  };
}

data[0]();	// 3
data[1]();	// 3
data[2]();	// 3

如果要强制返回预期的结果,怎么办???
方法1:立即执行函数

for (var i = 0; i < 3; i++) {
    (function(num) {
        setTimeout(function() {
            console.log(num);
        }, 1000);
    })(i);
}
// 0
// 1
// 2

方法2:返回一个匿名函数赋值

var data = [];

for (var i = 0; i < 3; i++) {
  data[i] = (function (num) {
      return function(){
          console.log(num);
      }
  })(i);
}

data[0]();	// 0
data[1]();	// 1
data[2]();	// 2

无论是立即执行函数还是返回一个匿名函数赋值,原理上都是因为变量的按值传递,所以会将变量i的值复制给实参num,在匿名函数的内部又创建了一个用于访问num的匿名函数,这样每个函数都有了一个num的副本,互不影响了。

方法3:使用ES6中的let

var data = [];

for (let i = 0; i < 3; i++) {
  data[i] = function () {
    console.log(i);
  };
}

data[0]();
data[1]();
data[2]();

解释下原理:

 var data = [];// 创建一个数组data;

// 进入第一次循环
{ 
	let i = 0; // 注意:因为使用let使得for循环为块级作用域
	           // 此次 let i = 0 在这个块级作用域中,而不是在全局环境中
    data[0] = function() {
    	console.log(i);
	};
}

循环时,let声明i,所以整个块是块级作用域,那么data[0]这个函数就成了一个闭包。这里用{}表达并不符合语法,只是希望通过它来说明let存在时,这个for循环块是块级作用域,而不是全局作用域。
上面的块级作用域,就像函数作用域一样,函数执行完毕,其中的变量会被销毁,但是因为这个代码块中存在一个闭包,闭包的作用域链中引用着块级作用域,所以在闭包被调用之前,这个块级作用域内部的变量不会被销毁。

// 进入第二次循环
{ 
	let i = 1; // 因为 let i = 1 和上面的 let i = 0     
	           // 在不同的作用域中,所以不会相互影响
	data[1] = function(){
         console.log(i);
	}; 
}
当执行data[1]()时,进入下面的执行环境。 
{ 
     let i = 1; 
     data[1] = function(){
          console.log(i);
     }; 
}
  • 闭包试题2
    代码1:
var scope = "global scope";
function checkscope(){
    var scope = "local scope";
    function f(){
        return scope;
    }
    return f;
}

checkscope()();          

代码2:

var scope = "global scope";
function checkscope(){
    var scope = "local scope";
    function f(){
        return scope;
    }
    return f;
}

var foo = checkscope(); 
foo();    

上面的两个代码中,checkscope()执行完成后,闭包f所引用的自由变量scope会被垃圾回收吗?为什么?

checkscope()执行完成后,代码1中自由变量特定时间之后回收,代码2中自由变量不回收。

首先要说明的是,现在主流浏览器的垃圾回收算法是标记清除,标记清除并非是标记执行栈的进出,而是从根开始遍历,也是一个找引用关系的过程,但是因为从根开始,相互引用的情况不会被计入。所以当垃圾回收开始时,从Root(全局对象)开始寻找这个对象的引用是否可达,如果引用链断裂,那么这个对象就会回收。

闭包中的作用域链中 parentContext.vo 是对象,被放在中,中的变量会随着执行环境进出而销毁,堆中需要垃圾回收,闭包内的自由变量会被分配到堆上,所以当外部方法执行完毕后,对其的引用并没有丢。

每次进入函数执行时,会重新创建可执行环境和活动对象,但函数的[[Scope]]是函数定义时就已经定义好的(词法作用域规则),不可更改。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

BMG-Princess

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值