Skip to content

Latest commit

 

History

History
438 lines (302 loc) · 15.6 KB

File metadata and controls

438 lines (302 loc) · 15.6 KB

作用域 等

函数作用域

function foo() {
 var a = 'iceman';
 console.log(a); // 输出"iceman"
}
foo();

含义是指,属于这个函数的全部变量都可以在整个函数的范围内使用及复用(事实上在嵌套的作用域中也可以使用)。例如:(var)

外部无法访问内部,内部可以访问外部(逐层向外寻找)

函数的作用域在函数创建时就已经确定了。当函数创建时,会有一个名为 [[Scopes]] 的内部属性保存所有父变量对象到其中。当函数执行时,会创建一个执行环境,然后通过复制函数的 [[Scopes]] 属性中的对象构建起执行环境的作用域链,然后,变量对象 VO 被激活生成 AO 并添加到作用域链的前端,完整作用域链创建完成:

在任意代码片段外部添加包装函数,可以将内部的变量和函数定义“隐藏”起来,外部作用域无法访问包装函数内部的任何内容。函数声明和函数表达式之间最重要的区别是它们的名称标识符将会绑定在何处。

(function foo(){ // <-- 添加 这 一行  
//不行。 foo 变量 名 被 隐藏 在 自身 中, 不会 非必要 地 污染 外部 作用域。
  var a = 3;
  console. log( a ); // 3
})(); // <-- 以及 这 一行

全局作用域

var b = 'programmer';
function foo() {
 console.log(b); // 输出"programmer"
}
foo();

作用域链

当代码在一个环境中执行时,会创建变量对象的一个作用域链。作用域链的用途是保证对执行环境有权访问的所有变量和函数的有序访问。

通过作用域链,我们可以访问到外层环境的变量和函数。

作用域 规定了如何查找变量,也就是确定当前执行代码对变量的访问权限。当查找变量的时候,会先从当前上下文的变量对象中查找,如果没有找到,就会从父级(词法层面上的父级)执行上下文的变量对象中查找,一直找到全局上下文的变量对象,也就是全局对象。这样由多个执行上下文的变量对象构成的链表就叫做 作用域链。

变量对象里边包含了执行上下文中所有变量和函数的声明,它的作用就是保证代码执行时对变量和函数的正确访问。如果在该变量对象中没有找到对应变量或函数,则会根据执行作用域链向上继续查找,

作用域链本质上是一个指向变量对象的指针列表(在文中我们使用数组表示),它只引用但不实际包含变量对象。

作用域链的前端始终都是当前执行上下文的变量对象,如果这个执行上下文属于函数执行上下文,则用活动对象作为变量对象。

全局执行上下文的变量对象(也就是全局对象)始终是作用域链的最后一个对象

词法作用域

词法作用域: 是由你在写代码时将变量和块作用域写在哪里来决定的。即: 无论函数在哪里被调用,也无论它如何被调用,它的词法作用域都只由函数被声明时所处的位置决定。

词法作用域是作用域的一种工作模型

编译时,而非运行时

function foo() {
  console. log( a ); 
}

function bar() {
  var a = 3; 
  foo();
}

var a = 2;
bar();// 会输出2

块级作用域

块级作用域: 在一个代码块(括在一对花括号中的一组语句)中定义的所有变量在代码块的外部是不可见的

es5, Javascript 并不支持块级作用域,就是因为有变量提升

块级声明用于声明在指定块的作用域之外无法访问的变量。

块级作用域的形式:

  1. 函数内部

  2. 块中(字符 { 和 } 之间的区域)

动态作用域

动态 作用域 并不 关心 函数 和 作用域 是 如何 声明 以及 在何处 声明 的, 只 关心 它们 从何 处 调用。 换句话说, 作用域 链 是 基于 调用 栈 的, 而 不是 代码 中的 作用域 嵌套。

需要 明确 的 是, 事实上 JavaScript 并不 具有 动态 作用域。 它 只有 词法 作用域, 简单 明了。 但是 this 机制 某种程度 上 很像 动态 作用域。
主要 区别: 词法 作用域 是在 写 代码 或者说 定义 时 确定 的, 而 动态 作用域 是在 运行时 确定 的。( this 也是!) 词法 作用域 关注 函数 在何处 声明, 而 动态 作用域 关注 函数 从何 处 调用。

let const var

声明的变量只在声明时的代码块内有效

不存在声明提升

存在暂时性死区,如果在变量声明前使用,会报错

var声明的变量为全局变量window/global,并且会将该变量添加为全局对象的属性,但是let和const不会。

var声明变量时,可以重复声明变量,后声明的同名变量会覆盖之前声明的遍历。const和let不允许重复声明变量。

let const 的区别

let和const都是ES6新增的用于创建变量的语法。 let创建的变量是可以更改指针指向(可以重新赋值)。但const声明的变量是不允许改变指针的指向。

变量

js的变量是松散类型的,就是说可以用来保存任何类型的数据。

赋值

var message。这样未经过初始化的变量,会保存一个特殊的值undefined

定义变量的同时,可以设置变量的值:var message = '22';

初始化值操作,并不会把他标记为固定类型,比如以上不会标记为字符串类型。

var定义局部变量

使用var操作符定义的变量将称为定义该变量的作用域中的局部变量。

这个变量在函数退出后就会销毁

function test(){
  var message = '2';
}
test();
alert(message); // 错误!

一条语句定义多个变量

var message = '2',
  flund = false,
  age = 2;

变量提升

//声明(变量,函数等)提升, 赋值不提升

var a=2;
//js编译阶段,先找到所有声明. var a;
//再进行执行阶段 a=2;

//函数优先变量提升 ,如 function foo(){}
foo(); // 1 var foo;
function foo() { console. log( 1 ); }
foo = function() { console. log( 2 ); };
//解析为:
function foo() { console. log( 1 ); }
foo(); // 1
foo = function() { console. log( 2 ); };

变量提升的表现是,无论我们在函数中何处位置声明的变量,好像都被提升到了函数的首部,我们可以在变量声明前访问到而不会报错。

造成变量声明提升的本质原因是 js 引擎在代码执行前有一个解析的过程,创建了执行上下文,初始化了一些代码执行时需要用到的对象。当我们访问一个变量时,我们会到当前执行上下文中的作用域链中去查找,而作用域链的首端指向的是当前执行上下文的变量对象,这个变量对象是执行上下文的一个属性,它包含了函数的形参、所有的函数和变量声明,这个对象的是在代码解析的时候创建的。这就是会出现变量声明提升的根本原因。

闭包

闭包正确的定义是:有权访问另一个函数内部变量的函数。简单说来,如果一个函数被作为另一个函数的返回值,并在外部被引用,那么这个函数就被称为闭包。

《JavaScript高级程序设计》这样描述: 闭包是指有权访问另一个函数作用域中的变量的函数;

《你不知道的JavaScript》这样描述:当函数可以记住并访问所在的词法作用域时,就产生了闭包,即使函数是在当前词法作用域之外执行。

闭包代码表示

function foo() {
  var a = 2;
  function bar() {
    console. log( a );
  }
  return bar;
}
// baz 就是闭包
var baz = foo();
baz(); // 2 ———— 朋友, 这 就是 闭 包 的 效果。

结合内存管理,垃圾回收机制来了解

可以手动把闭包变量设置=null,可以实现回收变量;

没有常规执行foo的垃圾回收机制,  
foo内的作用域依然存在,可以在baz中调用,  
闭包阻止了垃圾回收  
回调函数也是闭包  
个人总结function包住变量 的妙用

闭包本质原理

当闭包的父包裹函数执行完成后,父函数本身执行环境的作用域链会被销毁,但是由于闭包的作用域链仍然在引用父函数的变量对象,导致了父函数的变量对象会一直驻存于内存,无法销毁,除非闭包的引用被销毁,闭包不再引用父函数的变量对象,这块内存才能被释放掉。过度使用闭包会造成 内存泄露 的问题,这块等到闭包章节再做详细分析。

闭包与循环

//错误1
for (var i= 1; i<= 5; i++) {
  setTimeout( function timer() {
    console. log( i );
  }, i* 1000 );
}
//错误改进2  iife
for (var i= 1; i<= 5; i++) {
  (function() {
    setTimeout( function timer() {
      console. log( i );
    }, i* 1000 );
  })();
}
//正确
for (var i= 1; i<= 5; i++) {
  (function() {
    var j = i;
    setTimeout( function timer() {
      console. log( j );
    }, j* 1000 );
  })();
}
//改进
for (var i= 1; i<= 5; i++) {
  (function( j) {
    setTimeout( function timer() {
      console. log( j );
    }, j* 1000 );
  })( i );
}
//新用法
for (var i= 1; i<= 5; i++) {
  let j = i; // 是的, 闭 包 的 块 作用域!
  setTimeout( function timer() {
    console. log( j );
  }, j* 1000 );
}

闭包的作用

闭包的应用比较典型是定义模块 (我们将操作函数暴露给外部,而细节隐藏在模块内部)

1.封装变量

// 
var cache = {};
var mult = function(){
  var args = Array. prototype. join. call( arguments, ',' );
  if ( cache[ args ] ){
    return cache[ args ];
  }
  var a = 1;
  for ( var i = 0, l = arguments. length; i < l; i++ ){
    a = a * arguments[ i];
  }
  return cache[ args ] = a;
};
  alert ( mult( 1, 2, 3 ) ); // 输出: 6
  alert ( mult( 1, 2, 3 ) ); // 输出: 6

// 避免将cache变量和mult函数一起平行的暴露在全局作用域下
// 把cache变量封闭在mult函数内
// 可以优化为:
var mult = (function(){
  var cache = {};
  return function(){
    var args = Array. prototype. join. call( arguments, ',' );
    if ( args in cache ){
      return cache[ args ];
    }
    var a = 1;
    for ( var i = 0, l = arguments. length; i < l; i++ ){
      a = a * arguments[ i];
    }
    return cache[ args ] = a;
  }
})();

2.延续局部变量的寿命

var report = function( src ){
  var img = new Image();
  img. src = src;
};
report( 'http:// xxx. com/ getUserInfo' );
// 闭包优化后为:
// 保存了new 的img
var report = (function(){
  var imgs = [];
  return function( src ){
    var img = new Image();
    imgs. push( img );
    img. src = src;
  }
})();

闭包和面向对象的关系

通常用面向对象思想能实现的功能,闭包也能实现。反之亦然。对象以方法的形式包含了过程,而闭包是在过程中以以环境的形式包含了数据。

在JavaScript语言的祖先Scheme语言中,甚至都没有提供面向对象的原生设计,但可以使用闭包来实现一个完整的面向对象系统。

内存泄露

记录一次前端内存泄漏排查经历

一文理清由闭包引发内存泄漏和垃圾回收机制

执行上下文

JavaScript 引擎在执行一段可执行代码之前,会先进行准备工作,也就是对这段代码进行解析(也可以称为预处理)。这个 “准备工作”,就叫做 "执行上下文(execution context 简称 EC)" 或者也可以叫做执行环境。

执行上下文 为我们的可执行代码块提供了执行前的必要准备工作,例如变量对象的定义、作用域链的扩展、提供调用者的对象引用等信息。

这个阶段会根据可执行代码创建相应的执行上下文( Execution Context ),也就是做声明提升等工作(后边会详细讲解)。然后在代码解析完成后才开始代码的执行

JavaScript 引擎解析执行代码的过程是一个边执行边解析的过程,解析发生在执行一段可执行代码之前。举个例子,当执行到一个函数的时候,就会先对这个函数进行解析,然后再执行这个函数。

可执行代码

在代码解析阶段会根据不同的可执行代码创建相应的执行上下文 :

全局执行代码,在执行所有代码前,解析创建全局执行上下文。

函数执行代码,执行函数前,解析创建函数执行上下文。

eval执行代码,运行于当前执行上下文中。

执行上下文的组成

执行上下文定义了变量或函数有权访问的其他数据,决定了它们各自的行为。每一个执行上下文都由以下三个属性组成。

  1. 变量对象(Variable object,VO)

  2. 作用域链(Scope chain)

  3. 调用者信息(this)

如果将上述一个完整的执行上下文使用代码形式表现出来的话,应该类似于下面这种:

executionContext:{
  [variable object | activation object]{
      arguments,
      variables: [...],
      funcions: [...]
  },
  scope chain: variable object + all parents scopes
  thisValue: context object
}

变量对象(variable object 简称 VO)

每个执行环境文都有一个表示变量的对象——变量对象,全局执行环境的变量对象始终存在,而函数这样局部环境的变量,只会在函数执行的过程中存在,在函数被调用时且在具体的函数代码运行之前,JS 引擎会用当前函数的参数列表(arguments)初始化一个 “变量对象” 并将当前执行上下文与之关联 ,函数代码块中声明的 变量函数 将作为属性添加到这个变量对象上。

活动对象(activation object 简称 AO)

函数进入执行阶段时,原本不能访问的变量对象被激活成为一个活动对象,自此,我们可以访问到其中的各种属性。

其实变量对象和活动对象是一个东西,只不过处于不同的状态和阶段而已。

当前可执行代码块的调用者(this)

如果当前函数被作为对象方法调用或使用 bind call apply 等 API 进行委托调用,则将当前代码块的调用者信息(this value)存入当前执行上下文,否则默认为全局对象调用。

执行上下文栈

执行每一段可执行代码时都会对应创建一个执行上下文,那么我们是如何来管理这些执行上下文的呢?JavaScript 引擎创建了执行上下文栈(Execution context stack,ECS)来管理执行上下文。

JavaScript 开始要解释执行代码的时候,最先遇到的就是全局代码,所以 JavaScript 引擎会先解析创建全局执行上下文,然后将全局执行上下文压栈。然后当执行流进入一个函数时,会先解析创建函数的执行上下文,然后将它的执行上下文压栈。而在函数执行之后,会将其执行上下文弹栈,弹栈后执行上下文中所有的数据都会被销毁,然后把控制权返回给之前的执行上下文。注意,全局执行上下文会一直留在栈底,直到整个应用结束。

ES3 执行上下文的生命周期

创建阶段 执行阶段 销毁阶段

作用域题

var b = 10;
(function b() {
  b = 20;
  console.log(b)
})()
打印结果内容如下:
ƒ b() {
b = 20;
console.log(b)
}

原因:
作用域:执行上下文中包含作用于链:
在理解作用域链之前,先介绍一下作用域,作用域可以理解为执行上下文中申明的变量和作用的范围;包括块级作用域/函数作用域;
特性:声明提前:一个声明在函数体内都是可见的,函数声明优先于变量声明;
在非匿名自执行函数中,函数变量为只读状态无法修改;