【JavaScript忍者秘籍】读书笔记之第二部分:函数的定义与参数、函数调用、闭包和作用域

  • 3 函数定义与参数
    • 3.1 函数式的不同点到底是什么?
    • 3.2 回调函数
    • 3.3函数声明和函数表达式
    • 问题1:回调函数在哪种情况下会同步调用, 或者异步调用呢?
    • 问题2:箭头函数和函数表达式的区别是什么?
    • 问题3:你为什么需要在函数中使用默认参数?
  • 4 理解函数的调用
    • 4.1 使用隐式函数参数
    • 4.2 this参数:函数上下文
      • apply和call调用函数的区别:如何传递参数
      • 使用箭头函数绕过函数上下文
      • 函数还可访问bind方法创建新函数
    • 使用关键字new调用函数会触发以下几个动作:
    • 问题1:为什么this参数表示函数上下文?
    • 问题2:函数(function) 和方法(method) 之间有什么区别?
    • 问题3:如果一个构造函数显式地返回一个对象会发生什么?
  • 5 精通函数:闭包和作用域
    • 闭包
      • 什么是闭包?
      • 使用闭包注意点
      • 闭包的常见场景:回调函数
      • JavaScript引擎是如何跟踪函数的执行并回到函数的位置的呢?
      • 执行上下文
      • 使用词法环境跟踪变量的作用域
        • 为什么不直接跟踪整个执行上下文, 直接搜索与环境相匹配的标识符映射表呢?
    • JavaScript的变量类型
      • 3个关键字定义变量: var、 let和const的两点不同
      • 在作用域下的标识符
      • JavaScript代码的执行
      • 为什么可以在函数声明之前调用函数?
      • 函数重载-变量提升
    • 闭包的工作原理
    • 总结:
      • 闭包的原理?
      • 闭包的用处
      • JavaScript引擎是如何跟踪函数的执行并回到函数的位置的呢?
      • 闭包是JavaScript作用域规则的副作用,当函数创建时所在的作用消失后, 仍然能够调用函数。
      • 闭包消耗内存成本
      • 闭包访问的变量一定是变量,不包括对象内的属性

3 函数定义与参数

  • JavaScript中最关键的概念是: 函数是第一类对象

3.1 函数式的不同点到底是什么?

  • 函数是程序执行过程中的主要模块单元
  • JavaScript中函数拥有对象的所有能力
  • 唯一的特殊之处在于它是可调用的(invokable) , 即函数会被调用以便执行某项动作。

3.2 回调函数

  • 在执行过程中, 我们建立的函数会被其他函数在稍后的某个合适时间点“再回来调用”
  • sort()方法回调函数的期望返回值为: 如果传入值的顺序需要被调换, 返回正数; 不需要调换, 返回负数; 两个值相等, 返回0。

3.3函数声明和函数表达式

  • 函数声明
function hello(){
	return 'hello'
}
  • 函数表达式
let muFunc = function(){}
  • 对于函数声明来说, 函数名是强制性的, 而对于函数表达式来说, 函数名则完全是可选的.

问题1:回调函数在哪种情况下会同步调用, 或者异步调用呢?

问题2:箭头函数和函数表达式的区别是什么?

问题3:你为什么需要在函数中使用默认参数?

4 理解函数的调用

4.1 使用隐式函数参数

  • arguments参数并非JavaScript数组,仅是一个类数组的结构
  • arguments对象的主要作用是允许我们访问传递给函数的所有参数
  • 剩余参数rest是真正的Array实例, 也就是说你可以在它上面直接使用所有的数组方法
  • 在JavaScript提供的严格模式(strict mode) 中无法使用arguments

4.2 this参数:函数上下文

  • 不同类型函数调用之间的主要区别在于: 最终作为函数上下文(可以通过this参数隐式引用到) 传递给执行函数的对象不同
  • 可以使用apply和call方法显式地设置函数上下文。如果函数上下文与预期不符, 可以使用call或apply方法绕过
  • apply和call调用函数的区别:如何传递参数

  • 若想使用apply方法调用函数, 需要为其传递两个参数: 作为函数上下文的对象和一个数组作为函数调用的参数。 call方法的使用方式类似, 不同点在于是直接以参数列表的形式, 而不再是作为数组传递。
    【JavaScript忍者秘籍】读书笔记之第二部分:函数的定义与参数、函数调用、闭包和作用域_第1张图片【JavaScript忍者秘籍】读书笔记之第二部分:函数的定义与参数、函数调用、闭包和作用域_第2张图片
  • 如何选择?选择与现有参数相匹配的方法。 如果有一组无关的值, 则直接使用call方法。 若已有参数是数组类型, apply方法是更佳选择。

使用箭头函数绕过函数上下文

  • 箭头函数没有单独的this值
  • 它的this与声明所在的上下文相同
  • 调用箭头函数时, 不会隐式传入this参数, 而是从定义时的函数继承上下文

函数还可访问bind方法创建新函数

  • bind方法创建的新函数与原始函数的函数体相同, 新函数被绑定到指定的对象上
  • 所有函数均可使用bind方法, 创建新函数, 并绑定到bind方法传入的参数上。 被绑定的函数与原始函数具有一致的行为。

使用关键字new调用函数会触发以下几个动作:

1. 创建一个新的空对象
2. 该对象作为this参数传递给构造函数, 从而成为构造函数的函数上下文
3. 新构造的对象作为new运算符的返回值
【JavaScript忍者秘籍】读书笔记之第二部分:函数的定义与参数、函数调用、闭包和作用域_第3张图片

问题1:为什么this参数表示函数上下文?

  • 当调用函数时, 除了显式提供的参数外, this参数也会默认地传递给函数。 this参数是面向对象JavaScript编程的一个重要组成部分, 代表函数调用相关联的对象。 因此, 通常称之为函数上下文。

问题2:函数(function) 和方法(method) 之间有什么区别?

  • 通过()运算符调用一个函数, 且被执行的函数表达式不是作为一个对象的属性存在时, 就属于这种调用类型,非严格模式下this指向window,严格模式下this为undefined
  • 当一个函数被赋值给一个对象的属性, 并且通过对象属性引用的方式调用函数时, 函数会作为对象的方法被调用
  • 当函数作为某个对象的方法被调用时, 该对象会成为函数的上下文, 并且在函数内部可以通过参数访问到

问题3:如果一个构造函数显式地返回一个对象会发生什么?

  • 如果构造函数返回一个对象, 则该对象将作为整个表达式的值返回, 而传入构造函数的this将被丢弃
  • 但是, 如果构造函数返回的是非对象类型, 则忽略返回值, 返回新创建的对象

5 精通函数:闭包和作用域

  • 一个变量或方法有几种不同的作用域? 这些作用域分别是什么?
  • 如何定位标识符及其值?
  • 什么是可变变量? 如何在JavaScript中定义可变变量?

闭包

什么是闭包?

  • 闭包允许函数访问并操作函数外部的变量。 只要变量或函数存在于声明函数时的作用域内, 闭包即可使函数能够访问这些变量或函数。
  • 其实我们并没有意识到我们平时写在全局作用域的函数可以访问全局作用域的变量其实正是一种闭包行为
  • 注意: 当在外部函数中声明内部函数时, 不仅定义了函数的声明, 而且还创建了一个闭包。 该闭包不仅包含了函数的声明, 还包含了在函数声明时该作用域中的所有变量。当最终执行内部函数时, 尽管声明时的作用域已经消失了, 但是通过闭包, 仍然能够访问到原始作用域。
  • 即每一个通过闭包访问变量的函数都具有一个作用域链, 作用域链包含闭包的全部信息
    【JavaScript忍者秘籍】读书笔记之第二部分:函数的定义与参数、函数调用、闭包和作用域_第4张图片

使用闭包注意点

  • 由于原生JavaScript不支持私有变量,但是通过使用闭包,可以实现很接近的、可接受的私有变量
  • 可通过闭包内部方法获取私有变量的值, 但是不能直接访问私有变量,这有效地阻止了读私有变量不可控的修改。
    【JavaScript忍者秘籍】读书笔记之第二部分:函数的定义与参数、函数调用、闭包和作用域_第5张图片
  • 闭包内部的变量可以通过闭包内的方法访问,构造器外部的代码则不能访问闭包内部的变量

闭包的常见场景:回调函数

  • 通过在函数内部定义变量, 并基于闭包, 使得在计时器的回调函数中可以访问这些变量, 每个动画都能够获得属于自己的“气泡”中的私有变量。避免污染全局作用域。
    【JavaScript忍者秘籍】读书笔记之第二部分:函数的定义与参数、函数调用、闭包和作用域_第6张图片
  • 闭包内的函数不仅可以在创建的时刻访问这些变量, 而且当闭包内部的函数执行时, 还可以更新这些变量的值。 闭包不是在创建的那一时刻的状态的快照, 而是一个真实的状态封装, 只要闭包存在, 就可以对变量进行修改。

JavaScript引擎是如何跟踪函数的执行并回到函数的位置的呢?

  • 通过执行上下文来跟踪代码

执行上下文

  • 类别:全局执行上下文和函数执行上下文
  • 全局执行上下文只有一个,JavaScript开始执行时就已创建
  • 函数执行上下文是在每次调用函数时, 就会创建一个新的
  • 函数上下文和函数执行上下文是两个完全不一样的概念
  • 使用执行上下文栈(execution context stack 调用栈) 进行函数跟踪
  • 浏览器可以查看对应的调用栈
  • 在静态环境中通过执行上下文可以准确定位标识符实际指向的变量
    【JavaScript忍者秘籍】读书笔记之第二部分:函数的定义与参数、函数调用、闭包和作用域_第7张图片

使用词法环境跟踪变量的作用域

  • 词法环境(lexical environment) 是JavaScript引擎内部用来跟踪标识符与特定变量之间的映射关系,通常称为作用域
var a = '1'
console.log(a)

console.log语句访问a变量时, 会进行词法环境的查询。

  • 词法环境主要基于代码嵌套, 通过代码嵌套可以实现代码结构包含另一代码结构.
    【JavaScript忍者秘籍】读书笔记之第二部分:函数的定义与参数、函数调用、闭包和作用域_第8张图片- 在作用域范围内, 每次执行代码时, 代码结构都获得与之关联的词法环境。 例如, 每次调用skulk函数, 都将创建新的函数词法环境。
  • 内部代码结构可以访问外部代码结构中定义的变量。
  • JavaScript引擎是如何跟踪这些变量的呢? 如何判断可访问性呢? 这就是词法环境的作用。
  • 除了跟踪局部变量、 函数声明、 函数的参数和词法环境外, 还有必要跟踪外部(父级) 词法环境。 因为我们需要访问外部代码结构中的变量, 如果在当前环境中无法找到某一标识符, 就会对外部环境进行查找。 一旦查找到匹配的变量, 或是在全局环境中仍然无法查找到对应的标识符而返回错误, 就会停止查找。
  • 无论何时创建函数, 都会创建一个与之相关联的词法环境, 并存储在名为[[Environment]]的内部属性上(也就是说无法直接访问或操作)。 两个中括号用于标志内部属性
    【JavaScript忍者秘籍】读书笔记之第二部分:函数的定义与参数、函数调用、闭包和作用域_第9张图片

为什么不直接跟踪整个执行上下文, 直接搜索与环境相匹配的标识符映射表呢?

  • 实际上时候可行的,但是, 需要记住的是, JavaScript函数可以作为任意对象进行传递, 定义函数时的环境与调用函数的环境往往是不同的。
  • 最重要的部分: 外部环境与新建的词法环境, JavaScript引擎将调用函数的内置
    [[Environment]]属性与创建函数时的环境进行关联。
  • 即在图5.9中,当调用report函数时, 新创建的report环境的外部环境变成了skulk的环境。当report环境里没有action标识符的引用时,JavaScript引擎就需要查找report的外部环境: skulk环境。

JavaScript的变量类型

3个关键字定义变量: var、 let和const的两点不同

  • 可变性和与词法环境的关系(作用域)
可变性
const
只能在声明时初始化一次,便不允许再次赋值
let,var
可以变更任意次数
作用域
var
在距离最近的函数内部或是在全局词法环境中定义的
let,const
具有块级作用域,在距离最近的环境中定义

在作用域下的标识符

  • JavaScript对于在哪儿定义函数并不挑剔。 在调用函数之前或之后声明函数均可。

JavaScript代码的执行

  • 在第一阶段, 没有执行代码, 但是JavaScript引擎会访问并注册在当前词法环境中所声明的变量和函数。 JavaScript在第一阶段完成之后开始执行第二阶段, 具体如何执行取决于变量的类型(let、 var、 const和函数声明) 以及环境类型(全局环境、 函数环境或块级作用域) 。
    【JavaScript忍者秘籍】读书笔记之第二部分:函数的定义与参数、函数调用、闭包和作用域_第10张图片

为什么可以在函数声明之前调用函数?

  • fun是通过函数声明进行定义的, 第二阶段表明函数已通过函数声明进行定义, 在当前词法环境创建时已在其他代码执行之前注册了函数标识符。 所以, 在执行函数调用之前, fun函数已经存在。
  • 注意:函数表达式与箭头函数都不在此过程中, 而是在程序执行过程中执行定义的

函数重载-变量提升

  • 声明的变量与函数均使用相同的名字
  • 通过函数声明进行定义的函数在代码执行之前对函数进行创建, 并赋值给对应的标识符,之后处理变量的声明, 那些在当前环境中未声明的变量, 将被赋值为undefined
  • 变量的声明提升至函数顶部, 函数的声明提升至全局代码顶部,但是变量和函数的声明并没有实际发生移动。 只是在代码执行之前, 先在词法环境中进行注册。

闭包的工作原理

  • getFeints与feint函数是新创建的ninja的对象方法(如前文所述, 可通过this关键字访问) 。 因此, 可以在Ninja构造函数外部访问getFeints与feint函数, 这样实际上就创建了包含feints变量的闭包。
    【JavaScript忍者秘籍】读书笔记之第二部分:函数的定义与参数、函数调用、闭包和作用域_第11张图片
    【JavaScript忍者秘籍】读书笔记之第二部分:函数的定义与参数、函数调用、闭包和作用域_第12张图片

  • 通过闭包实现一种可接受的“私有”变量的方案
    【JavaScript忍者秘籍】读书笔记之第二部分:函数的定义与参数、函数调用、闭包和作用域_第13张图片

总结:

闭包的原理?

  • 通过闭包可以访问创建闭包时所处环境中的全部变量。 闭包为函数创建时所处的作用域中的函数和变量, 创建“安全气泡”。 通过这种的方式, 即使创建函数时所处的作用域已经消失, 但是函数仍然能够获得执行时所需的全部内容。

闭包的用处

  • 通过构造函数内的变量以及构造方法来模拟对象的私有属性。
  • 处理回调函数, 简化代码。

JavaScript引擎是如何跟踪函数的执行并回到函数的位置的呢?

  • JavaScript引擎通过==执行上下文栈(调用栈) ==跟踪函数的执行。 每次调用函数时, 都会创建新的函数执行上下文, 并推入调用栈顶端。当函数执行完成后, 对应的执行上下文将从调用栈中推出。
  • JavaScript引擎通过词法环境跟踪标识符(俗称作用域)

闭包是JavaScript作用域规则的副作用,当函数创建时所在的作用消失后, 仍然能够调用函数。

闭包消耗内存成本

  • 闭包始终保持创建所在的作用于内的变量

闭包访问的变量一定是变量,不包括对象内的属性

你可能感兴趣的:(JS,前端,javascript)