Skip to content
On this page

变量与作用域

原始值与引用值

保存原始值(6种)的变量是按值访问的,保存引用值(对象)的变量是按引用访问的。

  • 数据存储位置
    • 值存储——栈
    • 引用存储——引用信息存储在栈,内容存储在堆
  • 原因
    • 基本类型大小固定,占用空间小
    • 引用类型大小不固定,影响性能

原始值不能有属性,尽管尝试给原始值添加属性不会报错。

对于原始值和引用值的复制其实都是值复制,但原始值是复制值本身,引用值复制的是值存储的位置,仍然是引用同个对象。

函数传参只有按值传递,不管是原始值还是引用值。

js
let obj = {};
function setName(obj){
	// 地址1的obj
	obj.name = 'obj1';
	// 生成地址2的obj,并赋值给obj变量,原来的指向还是没有改变。
	obj = new Object();
	obj.name = 'obj2'
}
setName(obj);

console.log(obj.name) // 'obj1'

几个概念

  • 引擎:从头到尾负责整个JavaScript程序的编译和执行过程。
  • 编译器:负责语法分析及代码生成的内容。
  • 执行上下文:关联着一个变量对象,包含在这个上下文定义的所有变量和函数。
    • 全局上下文(windows对象)就是最外层的上下文,let或const声明的不会定义在全局上下文中,但作用域链的解析结果是一样的
    • 当函数进入执行流时,函数的上下文会被推进一个上下文栈中。
  • 作用域:负责收集和维护所有声明的变量组成的查询,并实施一套严格的规则来控制当前执行代码对变量的访问权限。
  • 作用域链:决定各级上下文访问变量和函数的顺序。在函数定义的时候已经确定,在函数对象中的scoped属性中,具体内容取决于定义的所在的位置。

引擎

查询标识符,分为LHS查询和RHS查询:

  • LHS:找到某个变量的容器本身,并对它赋值
    • 例如在第一个🌰中,a = 2 就是一个LHS查询
  • RHS:查找某个变量,获取它的值
    • 例如 console.log(a) 就是一个RSH查询

编译器

编译过程主要分为三个步骤:

  • 词法分析:将字符组成的字符串分解为代码块(词组单元)
  • 语法分析:将词组单元流转化为AST树(抽象语法🌲)
  • 代码生成:将AST树转化为机器可执行的代码。

举第一个🌰:var a = 1 ;

变量的赋值操作,实际上分为两步:

  1. 编译器会在当前的作用域中声明一个变量(如果没声明过),也就是 var a ;
  2. 在运行时,引擎会在作用域中查找该变量,如果能找到就进行赋值,也就是 a = 2,找不到就会报错。

词法作用域

  • 词法作用域Lexical Scopes)是 javascript 中使用的作用域类型,词法作用域 也可以被叫做 静态作用域,与之相对的还有 动态作用域(bash语言使用)。
  • 词法作用域为函数定义时所在的作用域。
  • 无论函数在哪里,在何时被调用,它的词法作用域都只在函数被声明时所处的位置决定。

例子

js
var a = 3;
function c(){
	alert(a);
}
(function(){
	var a = 4;
	c();
})();

js中变量的作用域链与定义时的环境有关,与执行时无关。

欺骗词法(不推荐使用)

  • eval(..)函数
    • 通常被用来执行动态创建的代码
    • 接受一个字符串,插入的字符串会被当做原本就在那里一样来处理。
    • 可能对原本的词法作用域进行了修改
  • with 关键字
    • 通常被当做重复引用同一个对象的多个属性快捷方式,可以不用重复引用对象本身
js
var obj = {
  a: 1,
  b: 2,
  c: 3
}
with(obj) {
  a = 3;
  b = 4;
  c = 5;
}
  • with声明实际上是根据传递的对象,凭空创建了一个全新的词法作用域,当对象中的标识符在全局作用域也找不到时,会自动创建一个全局变量。

函数作用域

属于这个函数的全部变量都可以在整个函数的范围内使用及复用。

  • 在外部使用一个函数包裹,可以隐藏内部实现。
  • 规避冲突
    • 使用全局命名空间
    • 使用模块管理
  • (function fn(){...})作为函数表达式,说明foo只能在...所代表的位置中被访问,外部作用域则不行,不会污染外部作用域。
  • 匿名函数表达式和具名函数表达式
    • 匿名的缺点
      • 在栈追踪中不会显示有意义的函数名,调试困难
      • 不方便自身引用
      • 代码可读性差
  • 立即执行函数表达式(IIFE)
    • (function(){})()
    • 用法
      • 通过函数作用域解决命名冲突
      • 把他们当做函数调用并传递参数进去
      • 倒置代码的运行数据

作用域嵌套

  • 实际情况中,通常需要同时顾及几个作用域。在当前作用域找不到时,引擎就会沿着作用域链在外层嵌套的作用域中继续查找,直到在全局作用域也找不到时,查找停止。
  • 作用域中的对象也有一个原型链,因此搜索可能设计每个对象的原型链。

可能发生的两种异常类型:

  • ReferenceError:作用域判别失败相关
  • TypeError:作用域判别成功,但对结果的操作是不合法的。

变量声明

var

  • 变量提升
    • 出现变量提升是因为Js的执行机制是 先编译再执行
    • 在编译阶段,变量和函数会被存放到变量环境中,变量的默认值会被设置为undefined;在代码执行阶段,JavaScript 引擎会从变量环境中去查找自定义的变量和函数。
    • 对于同名函数和变量的处理
      • 如果是同名的函数,JavaScript编译阶段会选择最后声明的那个。
      • 如果变量和函数同名,那么在编译阶段,变量的声明会被忽略。(函数提升优先于变量提升)
    • 函数提升会把整个函数挪到作用域顶部,变量提升只会把声明挪到作用域顶部
    • 每个作用域都会进行提升操作。
    • 重复的var声明会被忽略

例子

js
(function(){
   var x = y = 1;
})();
var z;

console.log(y); // 1
console.log(z); // undefined
console.log(x); // Uncaught ReferenceError: x is not defined

var x = y = 1; 实际上这里是从右往左执行的,首先执行y = 1, 因为y没有使用var声明,所以它是一个全局变量,然后第二步是将y赋值给x,讲一个全局变量赋值给了一个局部变量,最终,x是一个局部变量,y是一个全局变量,所以打印x是报错。

js
function a() {
    var temp = 10;
    function b() {
        console.log(temp); // 10
    }
    b();
}
a();

function a() {
    var temp = 10;
    b();
}
function b() {
    // 报错 Uncaught ReferenceError: temp is not defined
    console.log(temp);
}
a();

第二个函数a会覆盖第一个函数a,b函数中temp是全局中的temp变量,没有定义,所以为undefined

js
f = function() {return true;};   
g = function() {return false;};   
(function() {
   if (g() && [] == ![]) {   
      f = function f() {return false;};
      // function g() {return true;}  
      // 如果放在此处会报错:Uncaught TypeError: g is not a function
      // 放在此处。函数声明会提前,但是函数体不会提前,为undefined
   }
   function g() {return true;} // 放在此处,函数声明会提前
})();
console.log(g()); // false
console.log(f()); // false
  1. 自执行函数内,函数不会覆盖外面的函数
  2. [] == ![]:首先![]会转化为!true,为false,为0,而左边[]会被求值,为'',为0,所以条件成立。

let和const

用于定义块作用域,简单来说,花括号内 {...} 的区域就是块级作用域区域。

  • 重复的let声明会报错SyntaxError
  • 严格来讲,let在运行时也会被提升,但由于存在暂时性死区,所以实际中不能在声明之前使用。
    • 暂时性死区:用let/const声明的变量会先在作用域中被创建出来,但因此时还未进行词法绑定,所以是不能被访问的,如果访问就会抛出错误。因此,在这运行流程进入作用域创建变量,到变量可以被访问之间的这一段时间,就称之为暂时性死区。
  • const声明只应用于顶层对象,也就是说,声明对象,对象的属性还是可以修改的。
    • v8优化:运行时编译器会将所有实例都替换成实际的值,而不会通过查询表进行变量的查询。

闭包

什么是闭包

函数 A 内部有⼀个函数 B,函数 B 可以访 问到函数 A 中的变量,那么函数 B 就是闭包

形成闭包的原因

内部函数引用外部作用域的变量

js如何实现闭包

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

var foo = checkscope();
foo();

我们知道 f 执行上下文维护了一个作用域链:

bash
fContext = {
    Scope: [AO, checkscopeContext.AO, globalContext.VO],
}

就是因为这个作用域链,f 函数依然可以读取到 checkscopeContext.AO 的值,说明当 f 函数引用了 checkscopeContext.AO 中的值的时候,即使 checkscopeContext 被销毁了,但是 JavaScript 依然会让 checkscopeContext.AO 活在内存中,f 函数依然可以通过 f 函数的作用域链找到它,正是因为 JavaScript 做到了这一点,从而实现了闭包这个概念。

闭包的作用

  • 保护函数的私有变量不受外部的干扰。形成不销毁的栈内存。
  • 保存,把一些函数内的值保存下来。闭包可以实现方法和属性的私有化。

闭包的场景

  1. return一个函数
js
var n = 10
function fn(){
    var n =20
    function f() {
       n++;
       console.log(n)
     }
    return f
}

var x = fn()
x() // 21

fn()就是一个闭包,存在上级作用域的引用,即 n = 20,输出结果为21

2、函数作为参数

js
var a = 'merlin'
function foo(){
    var a = 'foo'
    function fo(){
        console.log(a)
    }
    return fo
}

function f(p){
    var a = 'f'
    p()
}
f(foo())
/* 输出
*   foo
/ 

fo()就是闭包,存在上级作用域的引用,即a = 'foo',console.log 结果为foo

3、自执行函数

js
var n = 'merlin';
(function p(){
    console.log(n)
})()
/* 输出
*   merlin
/ 

同样存在上级作用域的引用,即n = 'merlin',console.log 结果为merlin

4、循环赋值

js
for(var i = 0; i<10; i++){
  (function(j){
     setTimeout(function(){
      console.log(j)
      }, 1000) 
  })(i)
}

因为存在闭包,所以能依次输出0-9,闭包形成了10个互不干扰的私有作用域。每个作用域里的i都不同

如果去掉自执行函数,就不存在外部作用域的引用,那么每一个setTimeout调用的都是全局的i。

由于js是单线程的,遇到异步代码会先入栈,不先执行,等到同步代码执行完后才执行,此时的全局i为10,所以输入的为10个10

5、回调函数

js
window.name = 'merlin';
setTimeout(function timeHandler(){
  console.log(window.name);
}, 100);

闭包需要注意什么

容易导致内存泄漏。闭包会携带包含其它的函数作用域,因此会比其他函数占用更多的内存。

过度使用闭包会导致内存占用过多,所以要谨慎使用闭包。

例子

js
function fun(n, o) {
  console.log(o)
  return {
    fun: function(m){
      return fun(m, n);
    }
  };
}
var a = fun(0);  a.fun(1);  a.fun(2);  a.fun(3);
// a: {fun:function(m){ return fn(m, 0); }}
var b = fun(0).fun(1).fun(2).fun(3);
// b: {fun:function(m){ return fn(m, 0); }}
// b: {fun:function(m){ return fn(m, 1); }}
// b: {fun:function(m){ return fn(m, 2); }}
var c = fun(0).fun(1);  c.fun(2);  c.fun(3);
// b: {fun:function(m){ return fn(m, 0); }}
// b: {fun:function(m){ return fn(m, 1); }}

MIT Licensed | Copyright © 2021 - 2022