像素

浏览器中javascript的执行机制

变量提升

变量提示就是在 JavaScript 代码执行过程中,JavaScript 引擎把变量的声明部分和函数的声明部分提升到代码开头的“行为”。 变量被提升后,会给变量设置默认值,** undefined**。 JavaScript 代码执行过程中,需要先做变量提升,而之所以需要实现变量提升,是因为 JavaScript 代码在执行之前需要先编译。

  • 在编译阶段,变量和函数会被存放到变量环境中,变量的默认值会被设置为 undefined;在代码执行阶段,JavaScript 引擎会从变量环境中去查找自定义的变量和函数。
showName() // 函数声明被变量提示
console.log(myname) // myname的声明部分 被变量提升 初始值undefined
var myname = 'a'
function showName() {
    console.log('b');
}
// 打印顺序 b undefined
  • 如果在编译阶段,存在两个相同的函数,那么最终存放在变量环境中的是最后定义的那个,这是因为后定义的会覆盖掉之前定义的。
showName() // 函数声明被变量提示
function showName() {
    console.log('b');
}
function showName() {
    console.log('c');
}
// 此时打印 c
  • 函数声明提示权重要高于变量声明提示
showName() // 函数声明被变量提示
function showName() {
    console.log('b');
}
var showName = 3;
console.log(showName);
// 打印顺序 b 3

showName函数声明被提前,第一行执行函数,知道 showName = 3 执行完变量赋值,showName被覆盖。 JavaScript 的执行机制:先编译,再执行。

调用栈

调用栈就是用来管理函数调用关系的一种数据结构。具备后进先出的特点。 JavaScript 引擎正是利用栈的这种结构来管理执行上下文的。在执行上下文创建好后,JavaScript 引擎会将执行上下文压入栈中,通常把这种用来管理执行上下文的栈称为执行上下文栈,又称调用栈。 调用栈是 JavaScript 引擎追踪函数执行的一个机制,当一次有多个函数被调用时,通过调用栈就能够追踪到哪个函数正在被执行以及各函数之间的调用关系。

  • 每调用一个函数,JavaScript 引擎会为其创建执行上下文,并把该执行上下文压入调用栈,然后 JavaScript 引擎开始执行函数代码。
  • 如果在一个函数 A 中调用了另外一个函数 B,那么 JavaScript 引擎会为 B 函数创建执行上下文,并将 B 函数的执行上下文压入栈顶。
  • 当前函数执行完毕后,JavaScript 引擎会将该函数的执行上下文弹出栈。

利用浏览器查看调用栈的信息

  1. 打开“开发者工具”,点击“Source”标签,选择 JavaScript 代码的页面,然后加上断点,并刷新页面。通过右边“call stack”来查看当前的调用栈的情况。
  2. 还可以使用 console.trace() 来输出当前的函数调用关系。

栈溢出(Stack Overflow)

调用栈是有大小的,当入栈的执行上下文超过一定数目,JavaScript 引擎就会报错,我们把这种错误叫做栈溢出

块级作用域

由于 JavaScript 存在变量提升这种特性,从而导致了很多与直觉不符的代码,这也是 JavaScript 的一个重要设计缺陷。

作用域(scope)

作用域是指在程序中定义变量的区域,该位置决定了变量的生命周期。通俗地理解,作用域就是变量与函数的可访问范围,即作用域控制着变量和函数的可见性和生命周期。 三种作用域

  • 全局作用域中的对象在代码中的任何地方都能访问,其生命周期伴随着页面的生命周期。
  • 函数作用域就是在函数内部定义的变量或者函数,并且定义的变量或者函数只能在函数内部被访问。函数执行结束之后,函数内部定义的变量会被销毁。
  • 块级作用域,**es6中新增 **(目的为解决变量提升造成的不符合直觉的代码),通过关键字let、const使用。

块级作用域就是使用一对大括号包裹的一段代码,比如函数、判断语句、循环语句,甚至单独的一个{}都可以被看作是一个块级作用域。

变量提升所带来的问题

1. 变量容易在不被察觉的情况下被覆盖掉


var myname = "a"
function showName(){
  console.log(myname); // 输出 undefined
  if(0){
   var myname = "b"
  }
  console.log(myname); // 输出 undefined
}
showName()

函数内部作用域通过 var myname = "b" 进行了变量提升,将 myname = undefined 提升到 showName 函数作用域顶部。导致输出的结果和其他大部分支持块级作用域的语言都不一样。

2. 本应销毁的变量没有被销毁

function foo(){
  for (var i = 0; i < 7; i++) {
  }
  console.log(i);  // 7
}
foo()

创建执行上下文阶段,变量 i 就已经被提升了,所以当 for 循环结束之后,变量 i 并没有被销毁。

作用域链与闭包

作用域链

在每个执行上下文的变量环境中,都包含了一个外部引用,用来指向外部的执行上下文,这个外部引用称为 outer。 当一段代码使用了一个变量时,JavaScript 引擎首先会在“当前的执行上下文”中查找该变量,没有找到的话会从外部的执行上下文中接着寻找,这个查找的链条就称为作用域链。 在 JavaScript 执行过程中,其作用域链是由词法作用域决定的。

词法作用域

词法作用域就是根据代码的位置来决定的,是代码编译阶段就决定好的,和函数是怎么调用的没有关系。


function bar() {
    console.log(myName)
}
function foo() {
    var myName = "a"
    bar()
}
var myName = "b"
foo()

这里 bar 函数的外部执行上下文是全局执行上下文,所以根据作用域链查找myName,先在自己的执行上下文寻找,没有找到,去外层执行上下文,也就是全局执行上下文找。最终打印 “b”。

闭包

在 JavaScript 中,根据词法作用域的规则,内部函数总是可以访问其外部函数中声明的变量,当通过调用一个外部函数返回一个内部函数后,即使该外部函数已经执行结束了,但是内部函数引用外部函数的变量依然保存在内存中,我们就把这些变量的集合称为闭包。

const foo = () => { // 外部函数
  const name = 'b';
  const bar = () => { // 内部函数 bar 可以访问 外层作用域 name 形成了闭包
    console.log('name', name);
  }
  return bar;
}

const fn = foo();
fn(); // 打印 name b

闭包是怎么回收的

如果引用闭包的函数是一个全局变量,那么闭包会一直存在直到页面关闭;但如果这个闭包以后不再使用的话,就会造成内存泄漏。 如果引用闭包的函数是个局部变量,等函数销毁后,在下次 JavaScript 引擎执行垃圾回收时,判断闭包这块内容如果已经不再被使用了,那么 JavaScript 引擎的垃圾回收器就会回收这块内存。 原则:如果该闭包会一直使用,那么它可以作为全局变量而存在;但如果使用频率不高,而且占用内存又比较大的话,那就尽量让它成为一个局部变量。

this

在对象内部的方法中使用对象内部的属性是一个非常普遍的需求。但是 JavaScript 的作用域机制并不支持这一点,基于这个需求,JavaScript 又搞出来另外一套 this 机制。 this 是和执行上下文绑定的,也就是说每个执行上下文中都有一个 this。执行上下文中包含了变量环境、词法环境、外部环境还有 this

  • 在全局环境中调用一个函数,函数内部的 this 指向的是全局变量 window。
  • 通过一个对象来调用其内部的一个方法,该方法的执行上下文中的 this 指向对象本身。

this 的设计缺陷以及应对方案

1.嵌套函数中的 this 不会从外层函数中继承

var myObj = {
  name : "a", 
  showThis: function(){
    console.log(this)
    function bar(){console.log(this)}
    bar()
  }
}
myObj.showThis() // 打印 window 而不是 myObj

解决办法:

  1. 通过变量 存储this,把this的获取转换成作用域链的形式获取
var myObj = {
  name : "a", 
  showThis: function(){
    console.log(this)
    const that = this;
    function bar(){console.log(that)} // 此时that的查询是作用域链的形式
    bar()
  }
}
myObj.showThis() // myObj
  1. 通过箭头函数改造
var myObj = {
  name : "a", 
  showThis: function(){
    console.log(this)
    const bar = () => {console.log(this)}
    bar()
  }
}
myObj.showThis() // myObj

因为箭头函数没有执行上下文,也就没有this。所以箭头函数执行时this为其外部执行上下文的this。

2. 普通函数中的 this 默认指向全局对象 window

默认情况下调用一个函数,其执行上下文中的 this 是默认指向全局对象 window 的。

var myObj = {
  name : "a", 
  showThis: function(){
    function bar(){console.log(this)}
    bar()
  }
}
myObj.showThis() // 打印 window 而不是 myObj
  1. 通过call、apply改变this指向
var myObj = {
  name : "a", 
  showThis: function(){
    function bar(){console.log(this)}
    bar.call(this);
  }
}
  1. 启用严格模式,在严格模式下,默认执行一个函数,其函数的执行上下文中的 this 值是 undefined。