# 1. 作用域、this、闭包、执行上下文

# 1.1. 变量提升例题

如果像下面例子那样,采用function命令和var赋值语句声明同一个函数,由于存在函数提升,最后会采用var赋值语句的定义。

var f = function () {
  console.log('1');
}

function f() {
  console.log('2');
}

f() // 1

上面例子中,表面上后面声明的函数f,应该覆盖前面的var赋值语句,但是由于存在函数提升,实际上正好反过来。

# 1.2. 函数本身的作用域

函数本身也是一个值,也有自己的作用域。它的作用域与变量一样,就是其声明时所在的作用域,与其运行时所在的作用域无关。

var a = 1;
var x = function () {
  console.log(a);
};

function f() {
  var a = 2;
  x();
}

f() // 1

上面代码中,函数x是在函数f的外部声明的,所以它的作用域绑定外层,内部变量a不会到函数f体内取值,所以输出1,而不是2

总之,函数执行时所在的作用域,是定义时的作用域,而不是调用时所在的作用域

# 1.3. 闭包

function f1() {
  var n = 999;
  function f2() {
    console.log(n);
  }
  return f2;
}

var result = f1();
result(); // 999

上面代码中,函数f1的返回值就是函数f2,由于f2可以读取f1的内部变量,所以就可以在外部获得f1的内部变量了。

闭包就是函数f2,即能够读取其他函数内部变量的函数。由于在 JavaScript 语言中,只有函数内部的子函数才能读取内部变量,因此可以把闭包简单理解成“定义在一个函数内部的函数”。

闭包最大的特点,就是它可以“记住”诞生的环境,比如f2记住了它诞生的环境f1,所以从f2可以得到f1的内部变量。在本质上,闭包就是将函数内部和函数外部连接起来的一座桥梁。

闭包的最大用处有两个,一个是可以读取其他函数内部的变量,另一个就是让这些变量始终保持在内存中,即闭包可以使得它诞生环境一直存在。

请看下面的例子,闭包使得内部变量记住上一次调用时的运算结果。

function createIncrementor(start) {
  return function () {
    return start++;
  };
}

var inc = createIncrementor(5);

inc() // 5
inc() // 6
inc() // 7

上面代码中,start是函数createIncrementor的内部变量。通过闭包,start的状态被保留了,每一次调用都是在上一次调用的基础上进行计算。从中可以看到,闭包inc使得函数createIncrementor的内部环境,一直存在。所以,闭包可以看作是函数内部作用域的一个接口。

为什么会这样呢?原因就在于inc始终在内存中,而inc的存在依赖于createIncrementor,因此也始终在内存中,不会在调用结束后,被垃圾回收机制回收。

闭包的另一个用处,是封装对象的私有属性和私有方法

function Person(name) {
  var _age;
  function setAge(n) {
    _age = n;
  }
  function getAge() {
    return _age;
  }

  return {
    name: name,
    getAge: getAge,
    setAge: setAge
  };
}

var p1 = Person('张三');
p1.setAge(25);
p1.getAge() // 25

上面代码中,函数Person的内部变量_age,通过闭包getAgesetAge,变成了返回对象p1的私有变量。

注意,外层函数每次运行,都会生成一个新的闭包,而这个闭包又会保留外层函数的内部变量,所以内存消耗很大。因此不能滥用闭包,否则会造成网页的性能问题。

# 1.4. 函数表达式会变量提升吗?

fn1(); 
var fn1=function(){} // 报错(fn1 is not a function),函数表达式

fn2(); 
function fn2(){} // undefined,函数声明

可见,函数声明才会出现变量提升,函数表达式不会。 变量提升的其他例子:

console.log(a);
var a=9;         			 //undefined

fn6('zhang');
function fn6(name){
    console.log(name)
}    		                  // zhang

fn('zhang');
function fn(name){
    age =20; 
    console.log(name, age);
    var age
}                         //zhang 20

可以看出,函数提升就可以执行,变量只是声明。 再看几个例子:

fn('zhang');
function fn(name){
    console.log(name, age); 
    var age=10
}    			// zhang undefined

var fo = 1;
var foobar = function(){
    console.log(fo);
    var fo=2;
}
foobar();
// undefined
// 既不是1也不是2,因为函数中的 var fo 存在变量提升,会覆盖全局的 fo

# 1.5. 什么是执行上下文?

当代码运行时,会产生一个对应的执行环境,在这个环境中,所有变量会被事先提出来(变量提升),有的直接赋值,有的为默认值 undefined,代码从上往下开始执行,就叫做执行上下文。

在 JavaScript 中,运行环境有三种,分别是:

  1. 全局环境:代码首先进入的环境,也就是一段<script>
  2. 函数环境:函数被调用时执行的环境
  3. eval函数(不常用)

全局范围内什么会出现变量提升?

  1. 变量定义
  2. 函数声明

函数内什么会出现变量提升?

  1. 变量定义
  2. 函数声明
  3. this
  4. arguments

# 1.6. 执行上下文特点

  1. 单线程,在主进程上运行
  2. 同步执行,从上往下按顺序执行
  3. 全局上下文只有一个,浏览器关闭时会被弹出栈
  4. 函数的执行上下文没有数目限制
  5. 函数每被调用一次,都会产生一个新的执行上下文环境

# 1.7. 什么是执行上下文栈?

执行全局代码时,会产生一个执行上下文环境,每次调用函数都又会产生执行上下文环境。当函数调用完成时,这个上下文环境以及其中的数据都会被消除,再重新回到全局上下文环境。处于活动状态的执行上下文环境只有一个。 其实这是一个压栈出栈的过程——执行上下文栈。

执行上下文栈

函数中调用其他函数:

执行上下文栈

# 1.8. this使用场景

  1. 作为构造函数
  2. 作为对象属性
  3. 作为普通函数
  4. call apply bind

普通函数 ,this就是window

# 1.9. this的特点

this要在执行时才能确认值,定义时无法确认:

var a = {
    name : 'A',
    fn: function() {
        console.log(this.name)
    }
}

a.fn() // 此时 this 等于 a

a.fn.call({ name: 'b' }) // 此时 this 等于{ name: 'b' }

var fn1 = a.fn()
fn1() // 此时 this 等于 window

# 1.10. js 的作用域有哪些?

js没有块级作用域,只有函数和局部作用域:

// 无块级作用域
if (true) {
    var name = 'ZhangSan'
}
console.log(name) // 'ZhangSan'
// 函数和全局作用域
var a = 'LiBai'
function fn() {
    var a = 'DuFu'
    console.log(a)
}

console.log('global', a) // 'LiBai'

fn() // 'DuFu'

# 1.11. 什么是自由变量和作用域链?

  1. 当前作用域(函数或全局)没有定义的变量,就是自由变量。
  2. 当前函数没有定义a,就一层一层去父级作用域寻找,形成链式结构,成为作用域链。
var a = 100
function fn() {
    var b = 200
    console.log(a) // 当前作用域没有定义的变量,即自由变量
    console.log(b)
}

fn()

再看一个例子:

var a = 100
function fn1() {
    var b = 200
    function fn2() {
        var c = 300
        console.log(a) // 自由变量
        console.log(b) // 自由变量
        console.log(c)
    }
    fn2()
}

fn1()

# 1.12. 什么是js的闭包?有什么作用,用闭包写个单例模式

MDN对闭包的定义是:闭包是指那些能够访问自由变量的函数自由变量是指在函数中使用的,但既不是函数参数又不是函数的局部变量的变量

由此可以看出,闭包=函数+函数能够访问的自由变量,所以从技术的角度讲,所有JS函数都是闭包,但是这是理论上的闭包。还有一个实践角度上的闭包。

从实践角度上来说,只有满足1、即使创建它的上下文已经销毁,它仍然存在,2、在代码中引入了自由变量,才称为闭包。

闭包的应用:

  1. 模仿块级作用域
  2. 保存外部函数的变量
  3. 封装私有变量

闭包的实现 (opens new window) 单例模式,参见 前端面试手写代码 (opens new window)

# 1.13. 为什么闭包函数能够访问其他函数的作用域?

顺着作用域链一层一层往上找

# 1.14. 写一个自执行函数

( function(i){ console.log(i)} ) (2)    // 2

# 1.15. 判断打印顺序

for(var i = 0; i < 5; i++){
  setTimeout((function() {
    return function() {
      console.log(i++)
    }
  })(), 0)
}
// 5 6 7 8 9

# 1.15. 判断打印结果

var length = 10
function fn() {
  console.log(this.length)
}
var obj = {
  length: 5,
  method: function(fn) {
    fn()
    arguments[0]()
  }
}

obj.method(fn, 1)
// 10
// 2