学习作用域、变量提升、闭包等语言特征,加深对 JavaScript 的理解,掌握变量赋值、函数声明的简洁语法,降低代码的冗余度。

  • 理解作用域对程序执行的影响
  • 能够分析程序执行的作用域范围
  • 理解闭包本质,利用闭包创建隔离作用域
  • 了解什么变量提升及函
  • 数提升
  • 掌握箭头函数、解析剩余参数等简洁语法

    一、作用域

    了解作用域对程序执行的影响及作用域链的查找机制,使用闭包函数创建隔离作用域避免全局变量污染。

作用域(scope)规定了变量能够被访问的“范围”,离开了这个“范围”变量便不能被访问,作用域分为全局作用域和局部作用域。

1.1 局部作用域

局部作用域分为函数作用域和块作用域。

函数作用域

在函数内部声明的变量只能在函数内部被访问,外部无法直接访问。

  1. <script>
  2. // 声明 counter 函数
  3. function counter(x, y) {
  4. // 函数内部声明的变量
  5. let s = x + y;
  6. console.log(s); // 18
  7. }
  8. // 设用 counter 函数
  9. counter(10, 8);
  10. // 访问变量 s
  11. console.log(s); // 报错
  12. </script>

总结:

  1. 函数内部声明的变量,在函数外部无法被访问
  2. 函数的参数也是函数内部的局部变量
  3. 不同函数内部声明的变量无法互相访问
  4. 函数执行完毕后,函数内部的变量实际被清空了

    块作用域

    在 JavaScript 中使用 {} 包裹的代码称为代码块,代码块内部声明的变量外部将【有可能】无法被访问。

    <script>
    {
     // age 只能在该代码块中被访问
     let age = 18;
     console.log(age); // 正常
    }
    
    // 超出了 age 的作用域
    console.log(age); // 报错
    
    let flag = true;
    if(flag) {
     // str 只能在该代码块中被访问
     let str = 'hello world!';
     console.log(str); // 正常
    }
    
    // 超出了 age 的作用域
    console.log(str); // 报错
    
    for(let t = 1; t <= 6; t++) {
     // t 只能在该代码块中被访问
     console.log(t); // 正常
    }
    
    // 超出了 t 的作用域
    console.log(t); // 报错
    </script>
    

    JavaScript 中除了变量外还有常量,常量与变量本质的区别是【常量必须要有值且不允许被重新赋值】,常量值为对象时其属性和方法允许重新赋值。

    <script>
    // 必须要有值
    const version = '1.0.0';
    
    // 不能重新赋值
    // version = '1.0.1';
    
    // 常量值为对象类型
    const user = {
     name: '小明',
     age: 18
    }
    
    // 不能重新赋值
    user = {};
    
    // 属性和方法允许被修改
    user.name = '小小明';
    user.gender = '男';
    </script>
    

    总结:

  5. let 声明的变量会产生块作用域,var 不会产生块作用域

  6. const 声明的常量也会产生块作用域
  7. 不同代码块之间的变量无法互相访问
  8. 推荐使用 letconst

注:开发中 letconst 经常不加区分的使用,如果担心某个值会不小被修改时,则只能使用 const 声明成常量。

关键字 块级作用域 变量提升 初始值 更改值 通过window调用
let ×√ - Yes No
const ×√ Yes No No
var × - Yes Yes

1.2 全局作用域

<script> 标签和 .js 文件的【最外层】就是所谓的全局作用域,在此声明的变量在函数内部也可以被访问。

<script>
  // 此处是全局

  function sayHi() {
    // 此处为局部
  }

  // 此处为全局
</script>

全局作用域中声明的变量,任何其它作用域都可以被访问,如下代码所示:

<script>
    // 全局变量 name
    let name = '小明';

      // 函数作用域中访问全局
    function sayHi() {
      // 此处为局部
      console.log('你好' + name);
    }

    // 全局变量 flag 和 x
    let flag = true;
    let x = 10;

      // 块作用域中访问全局
    if(flag) {
      let y = 5;
      console.log(x + y); // x 是全局的
    }
</script>

总结:

  1. window 对象动态添加的属性默认也是全局的,不推荐!
  2. 函数中未使用任何关键字声明的变量为全局变量,不推荐!!!
  3. 尽可能少的声明全局变量,防止全局变量被污染

JavaScript 中的作用域是程序被执行时的底层机制,了解这一机制有助于规范代码书写习惯,避免因作用域导致的语法错误。

1.3 作用域链

在解释什么是作用域链前先来看一段代码:

<script>
  // 全局作用域
  let a = 1;
  let b = 2;

  // 局部作用域
  function f() {
    let c;
    // 局部作用域
    function g() {
      let d = 'yo';
    }
  }
</script>

函数内部允许创建新的函数,f 函数内部创建的新函数 g,会产生新的函数作用域,由此可知作用域产生了嵌套的关系。
如下图所示,父子关系的作用域关联在一起形成了链状的结构,作用域链的名字也由此而来。
image.png
作用域链本质上是底层的变量查找机制,在函数被执行时,会优先查找当前函数作用域中查找变量,如果当前作用域查找不到则会依次逐级查找父级作用域直到全局作用域,如下代码所示:

<script>
  // 全局作用域
  let a = 1;
  let b = 2;

  // 局部作用域
  function f() {
    let c;
    // let a = 10;
    console.log(a); // 1 或 10
    console.log(d); // 报错

    // 局部作用域
    function g() {
      let d = 'yo';
      // let b = 20;
      console.log(b); // 2 或 20
    }

    // 调用 g 函数
    g()
  }

  console.log(c); // 报错
  console.log(d); // 报错

  f();
</script>

总结:

  1. 嵌套关系的作用域串联起来形成了作用域链
  2. 相同作用域链中按着从小到大的规则查找变量
  3. 子作用域能够访问父作用域,父级作用域无法访问子级作用域(就近原则)

    1.4 闭包

    闭包是一个函数和其作用域对象组成的结果,使用闭包能够访问函数作用域中的变量。从代码形式上看闭包是一个做为返回值的函数,如下代码所示:

    <script>
    function foo() {
     let i = 0;
    
     // 函数内部分函数
     function bar() {
             console.log(++i);
     }
    
     // 将函数做为返回值
     return bar;
    }
    
    // fn 即为闭包函数
    let fn = foo();
    
    fn(); // 1
    </script>
    

    面试题: ```javascript function fn1() { let i = 1 function fn2() { console.log(i) i++ } return fn2 }

let fanhuizhi = fn1() fanhuizhi() // 1 fanhuizhi() // 2 fanhuizhi() // 3

总结:<br />闭包:一个作用域有权访问另外一个作用域的局部变量。<br />好处:可以把一个变量使用范围延伸

1. 闭包本质仍是函数,只不是从函数内部返回的
2. 闭包能够创建外部可访问的隔离作用域,避免全局变量污染
3. **过度使用闭包可能造成内存泄漏(内存占用过多)**

现在闭包用的非常少了。因为有块级作用域。<br />注:回调函数也能访问函数内部的局部变量。
<a name="MkviC"></a>
## 1.5 变量提升
变量提升是 JavaScript 中比较“奇怪”的现象,它允许在变量声明之前即被访问,
```html
<script>
  // 访问变量 str
  console.log(str + 'world!');

  // 声明变量 str
  var str = 'hello ';
</script>

let和var都有提升,但是let定义的变量没有赋值之前是不可以使用、var可以使用是undefined

面试题:

var b = 100
function fn() {
  console.log(b)
  var b = 200
}

fn()   // 结果是 undefined

面试题:

{
  b = 3  // 声明变量的时候,没有加任何关键字
}
console.log(b) // 3

// 没有任何关键字声明的变量,这种变量相当于是全局变量
a = 1
function fn() {
  a = 200  // 没有任何关键字,声明了变量,相当于是全局变量
}
fn()

console.log(a) // 200

面试题:

// 用let声明的变量也可以认为是有变量提升的;只不过在没有给变量赋值之前是不能使用的(使用就会报错)
// console.log(a)
// let a = 2

// -----------------------------------------------------
let b = 100
function fn() {
  console.log(b)  // 报错,因为会把下一行的b的声明提升到前面,提升后又没有初始值,还断绝了去外层作用域查找的机会
  // 在变量提升后,一直到变量赋值之前,这个时间段叫做暂时性死区。这个时候不能使用变量
  let b = 500
}
fn()

总结:

  1. 变量在未声明即被访问时会报语法错误
  2. 用var声明的变量在声明之前即被访问,变量的值为 undefined
  3. let声明的变量,在声明之前使用,就会报错
  4. let 声明的变量不存在变量提升,推荐使用 let【也有人认为具有提升但是不赋值不能使用】
  5. 变量提升出现在相同作用域当中
  6. 实际开发中推荐先声明再访问变量,必须的

注:关于变量提升的原理分析会涉及较为复杂的词法分析等知识,而开发中使用 let 可以轻松规避变量的提升,因此在此不做过多的探讨,有兴趣可查阅资料

二、函数

知道函数参数默认值、动态参数、剩余参数的使用细节,提升函数应用的灵活度,知道箭头函数的语法及与普通函数的差异。

2.1 函数提升

函数提升与变量提升比较类似,是指函数在声明之前即可被调用。

<script>
  // 调用函数
  foo();

  // 声明函数
  function foo() {
    console.log('声明之前即被调用...');
  }

  // 不存在提升现象
  bar();
  var bar = function () {
    console.log('函数表达式不存在提升现象...');
  }
</script>

课堂代码:

// ------------------------- 简单体会函数提升 -----------------------
// 代码运行时,会将函数的声明和创建提升到当前作用域的最前面(还要在提升的变量之前)
// fn()

// console.log(111)
// console.log(222)

// function fn() {
//   console.log('我会被提升到当前作用域的最前面')
// }

// -------------------------- 函数表达式 --------------------------
// 函数表达式,实际上就是一个变量的声明,所以没有这样的函数提升,有的只是变量提示
// fn1() // 报错,表达式函数,只是一个变量而已,不具有函数提示特点
// let a = 1
// let fn1 = function () {
//   console.log('我是函数,只是一个变量的值而已,所以把我当做变量对待')
// }

// ----------------------- 函数提升和变量提升同时存在 ------------------
// 如果变量提升和函数提示同时存在,则函数提升更靠前

console.log(abc) // 函数abc
var abc = 100
function abc() {
  console.log('我是函数')
}

console.log(abc) // 100

// ----------------------- 重新声明变量(不给值),则不会影响变量原来的值 ----------------
// var a = 100
// var a // 再次声明变量,但是没有给值,所以不会改变变量原来的值
// console.log(a) // 100

总结:

  1. 函数提升能够使函数的声明调用更灵活
  2. 函数表达式不存在提升的现象
  3. 函数提升出现在相同作用域当中

    2.2 参数

    函数参数的使用细节,能够提升函数应用的灵活度。

    默认值

    <script>
    // 设置参数默认值
    function sayHi(name="小明", age=18) {
     document.write(`<p>大家好,我叫${name},我今年${age}岁了。</p>`);
    }
    // 调用函数
    sayHi();
    sayHi('小红');
    sayHi('小刚', 21);
    </script>
    
    课堂代码: ```javascript // ——————————- 正常的函数参数默认值 ———————————- // function abc(m, n = 200) { // console.log(m + n) // } // abc(2) // abc(8) // abc(7, 50) // abc(1)

// ——————— 有默认 值的参数尽量放到所有参数的最后 ——————— // function fn(x = 10, y) { // console.log(x + y) // }

// fn(undefined, 5)

function fn(y, x = 10) { console.log(x + y) } fn(5)

总结:

1. 声明函数时为形参赋值即为参数的默认值
2. 如果参数未自定义默认值时,参数的默认值为 `undefined`
3. 调用函数时没有传入对应实参时,参数的默认值被当做实参传入
<a name="T9RGG"></a>
### 动态参数
`arguments` 是函数内部内置的伪数组变量,它包含了调用函数时传入的所有实参。
```html
<script>
  // 求生函数,计算所有参数的和
  function sum() {
    // console.log(arguments);
    let s = 0;
    for(let i = 0; i < arguments.length; i++) {
      s += arguments[i];
    }
    console.log(s);
  }

  // 调用求和函数
  sum(5, 10); // 两个参数
  sum(1, 2, 4); // 两个参数
</script>

总结:

  1. arguments 是一个伪数组
  2. arguments 的作用是动态获取函数的实参

    剩余参数(ES6新增)

    <script>
    function config(baseURL, ...other) {
     console.log(baseURL);
     // other 是真数组,动态获取实参
     console.log(other);
    }
    
    // 调用函数
    config('http://baidu.com', 'get', 'json');
    </script>
    

    总结:

  3. ... 是语法符号,置于最末函数形参之前,用于获取多余的实参

  4. 借助 ... 获取的剩余实参

    2.3 箭头函数

    箭头函数是一种声明函数的简洁语法,它与普通函数并无本质的区别,差异性更多体现在语法格式上。

    <script>
    // 箭头函数
    let foo = () => {
     console.log('^_^ 长相奇怪的函数...');
    }
    // 调用函数
    foo();
    
    // 更简洁的语法
    let form = document.querySelector('form');
    form.addEventListener('click', ev => ev.preventDefault());
    </script>
    

    箭头函数的简化写法: ```javascript // 1. 如果箭头函数的形参有1个,可以省略 () // let fn = x => { // console.log(x * x) // } // fn(7)

// 2. 如果箭头函数的函数体,只有一行代码,则可以省略大括号。并且表示 return 这个函数体 // let fn = (x) => { // return x * x // } // console.log(fn(8))

let fn = x => x x // 默认,就表示 return x x console.log(fn(6))

// 如果函数需要返回一个 {} 空对象,则不能向下面那样 let fn = () => {} // 这样写的话,{}相当于是函数的大括号 let fn = () => ({}) // 这样写的话,相当于返回了 {} 空对象

总结:

1. 箭头函数属于表达式函数,因此不存在函数提升,只能先定义,然后再使用
2. 箭头函数只有一个参数时可以省略圆括号 `()`
3. 箭头函数函数体只有一行代码时可以省略花括号 `{}`,并自动做为返回值被返回
4. 箭头函数中没有 `arguments`,只能使用 `...` 动态获取实参
5. 涉及到this的使用,不建议用箭头函数
5. 箭头函数,一般都是当做回调函数使用。
5. 如果函数需要返回对象 `{}` ,则需要在 `{}`外面加一层 `()`
<a name="irzql"></a>
# 三、展开运算符(ES6新增)

- 在构造数组时,可以将其他数组展开
- 在构造数组时,可以将字符串展开
- 在调用函数时,可以将数组展开
- 在调用函数时,可以将字符串展开
- 在创建字面量对象时,可以将其他对象按key-value的形式展开
```javascript
// 1. 在创建新数组(arr3)时,可以把其他数组(arr1和arr2)展开
// let arr1 = [3, 4, 5]
// let arr2 = [8, 9, 6]
// let arr3 = [...arr1, ...arr2, 0, 2, 7]

// console.log(arr3)

// 2. 在创建数组(arr4)时,可以把字符串('nice')展开
// let arr4 = [...'nice']
// console.log(arr4)

// 3. 创建新的字面量对象时,可以将其他对象展开
// let obj1 = { id: 1, name: 'lisi' }
// let obj2 = { height: 180, weight: 75 }
// let obj3 = {
//   ...obj1,
//   ...obj2,
//   nickname: '及时雨'
// }
// console.log(obj3)

// 4. 调用函数时,注意,是调用函数时,可以将数组展开
// function fn(x, y, z) {
//   console.log(x, y, z)
// }

// let arr5 = [4, 2, 5]
// fn(...arr5) // 调用函数,可以将数组展开,相当于 fn(4, 2, 5)

// console.log(Math.max(4, 2, 8, 0, 9, 3))
// let arr6 = [4, 2, 8, 0, 9, 3] // 找出数组中最大的值
// console.log(Math.max(...arr6))
// console.log(Math.min(...arr6))


// 5. 调用函数时,可以将字符串展开
// function fn(a, b, c, d) {
//   console.log(a, b, c, d)
// }
// fn(...'nice')  // fn('n', 'i', 'c', 'e')

四、解构赋值

知道解构的语法及分类,使用解构简洁语法快速为变量赋值。

解构赋值是一种快速为变量赋值的简洁语法,本质上仍然是为变量赋值,分为数组解构、对象解构两大类型。

4.1 数组解构

数组解构是将数组的单元值快速批量赋值给一系列变量的简洁语法,如下代码所示:
解开数据结构赋值给变量==>快速取值

<script>
  // 普通的数组
  let arr = [1, 2, 3];
  // 批量声明变量 a b c 
  // 同时将数组单元值 1 2 3 依次赋值给变量 a b c
  let [a, b, c] = arr;
  console.log(a); // 1
  console.log(b); // 2
  console.log(c); // 3
</script>

课堂代码:

// 解构赋值
// 解:解开、展开
// 构:结构  ------------ JS中,有结构的数据类型:数组、对象
// 赋值:把值赋值给变量

// 解构赋值:把数组 或者 对象,展开,将里面的值取出,赋值给一些变量

// let arr = ['张飞', '李逵', '老段']
// let a = arr[0]
// let b = arr[1]
// let c = arr[2]

// ------------------ 演示基本的数组解构赋值 -------------------
// let [a, b, c] = ['张飞', '李逵', '大飞哥']
// console.log(a, b, c)


// -------------------- 数组解构赋值的细节问题 ----------------
// let [a, b, c] = ['张飞', '李逵', '大飞哥', '老段', '秋姐']
// console.log(a, b, c)

// let [a, b, c, d] = ['张飞', '李逵']
// console.log(a, b, c, d)

// let [a, b, ...c] = ['张飞', '李逵', '大飞哥', '老段', '秋姐']
// console.log(a, b, c) // 张飞 李逵 ['大飞哥', '老段', '秋姐']

// let [, , a, b] = ['张飞', '李逵', '大飞哥', '老段', '秋姐']
// console.log(a, b)

// let [a, b, [c, d], [e, f]] = [3, 5, [2, 1], [4, 8]]
let [, , [c], [, f]] = [3, 5, [2, 1], [4, 8]]
console.log(c, f)

总结:

  1. 赋值运算符 = 左侧的 [] 用于批量声明变量,右侧数组的单元值将被赋值给左侧的变量
  2. 变量的顺序对应数组单元值的位置依次进行赋值操作
  3. 变量的数量大于单元值数量时,多余的变量将被赋值为 undefined
  4. 变量的数量小于单元值数量时,可以通过 ... 获取剩余单元值,但只能置于最末位
  5. 允许初始化变量的默认值,且只有单元值为 undefined 时默认值才会生效

注:支持多维解构赋值,比较复杂后续有应用需求时再进一步分析

4.2 对象解构

对象解构是将对象属性和方法快速批量赋值给一系列变量的简洁语法,如下代码所示:

<script>
  // 普通对象
  let user = {
    name: '小明',
    age: 18
  };

  // 批量声明变量 name age
  // 同时将数组单元值 1 2 3 依次赋值给变量 a b c
  let {name, age} = user;

  console.log(name); // 小明
  console.log(age); // 18
</script>

课堂代码:

// ----------------- 基本的解构 ----------------
// let { id, name, age } = { id: 1, name: 'zs', age: 30 }
// console.log(id, name, age)

// ----------------- 细节问题 --------------------
// let { id, name, nianling } = { id: 1, name: 'zs', age: 30 }
// console.log(id, name, nianling) // 1 'zs' undefined // 要求变量的名字,必须和对象的键一样才能解构

// let { age, id, name } = { id: 1, name: 'zs', age: 30 }
// console.log(id, name, age) // 变量的顺序不做要求


// let name = '老段'
// let { age, id, name: xingming } = { id: 1, name: 'zs', age: 30 }
// console.log(id, xingming, age) // 通过 ”name:xingming” 这样的语法,给新变量改个名字


let person = {
  name: '老段',
  age: 30,
  dog: {
    name: '旺财',
    age: 3
  }
}

// let { dog: { name, age } } = { name: '老段', age: 30, dog: { name: '旺财', age: 3 } }
let { dog: { name, age } } = person
console.log(name, age)

// let { name, age } = person.dog
// console.log(name, age)

总结:

  1. 赋值运算符 = 左侧的 {} 用于批量声明变量,右侧对象的属性值将被赋值给左侧的变量
  2. 对象属性的值将被赋值给与属性名相同的变量
  3. 对象中找不到与变量名一致的属性时变量值为 undefinedsexa
  4. 允许初始化变量的默认值,属性不存在或单元值为 undefined 时默认值才会生效

注:支持多维解构赋值,比较复杂后续有应用需求时再进一步分析

五、总结

  • 全局作用域和局部作用域
    • 全局作用域,<script>标签内部。还有 引入的 xxx.js 的最外层
    • 局部作用域
      • 函数作用域
      • 块级作用域
  • 作用域链
    • 说的就是变量的查找过程
    • 在某个作用域中,使用一个变量的时候,优先使用当前作用于的 变量
    • 如果当前作用域没有这个变量,则向上层查找
    • ………..
    • 一直找到全局作用域,如果还没有找到,则报错
  • 闭包
    • 闭包是 一个函数及其函数作用域所组成的一个组合。
    • 函数中,如果返回了另外一个函数,则会形成闭包
    • 使用了闭包之后,函数会永驻内存,不会释放
    • 为了避免内存泄漏(内存占用过多),当我们不用闭包的时候,可以让 ”返回值=null
  • 提升
    • 用function声明的函数的提升,函数的提升会提升到当前作用域的前面
    • var声明的变量具有变量提升,提升也仅仅是把变量的声明过程提升(赋值还是要在后面赋值的)
  • 代码类的,自行查看代码