重学前端笔记

141 阅读26分钟

学习前端的方法:

第一个方法: 建立知识架构

第一个方法是建立自己的知识架构,并且在这个架构上,不断地进行优化。

 

前端知识架构图

什么是知识架构?

可以把它理解为知识的目录或者索引,它能够帮助我们把零散的知识组织起来,也能够帮助我们发现一些知识上的盲区。

如果要给JavaScript知识做一个顶层目录,该怎么做呢?

可以这样划分:

JavaScript

  • 文法
    • 词法
    • 语法
  • 语义
  • 运行时
    • 数据结构
      • 类型
        • 对象
      • 实例
        • 应用和机制
    • 执行过过程(算法)
      • 事件循环
      • 微任务的执行
      • 函数的执行
      • 语句级的执行

 

 

image.png

 

解释:

在JavaScript的模块中,首先我们可以把语言按照文法、语义和运行时来拆分,

文法可以分为词法和语法,这来自编译原理的划分;

这符合编程语言的一般规律: 用一定的词法和语法,表达一定语义,从而操作运行时。

接下来,我们又按照程序的一般规律,把运行时分为数据结构和算法部分。

数据结构包含类型和实例(类型系统就是它的7种基本类型和7种语言类型,实例就是内置对象部分), 所谓的算法,就是JavaScript的执行过程。

 

文法中的语法和语义基本上是一一对应关系。

 

HTML和CSS

image.png

 

浏览器的实现原理和API

image.png

浏览器部分心介绍浏览器的实现原理,这是我们深入理解API的基础。

我们一般会从浏览器设计出发,按照解析。构建DOM树、计算CSS、渲染、合成和绘制的流程来讲解浏览器的工作原理。

在API部分,我们会从w3c零散的标准中挑选几个大块的API来详细讲解,主要有事件,DOM、CSSOM这几个部分,他们主要覆盖了交互、语义和可见效果,这是我们工作中用到的主要内容。

 

前端工程实践

image.png

最后一个模块时前端工程实践。我们再掌握了前面的基础知识之后,也就基本掌握了做一个前端工程师的底层能力。

性能

首先我们会谈性能,对任何一个前端团队而言,性能是它价值的核心指标。

 

工具链

下一个案例是工具链。这一部分,我将会探讨企业中工具链的建设思路。对一个高效又合作良好的前端团队来说,一致性的工具链是不可或缺的保障,作为开发阶段的入口,工具链又可以和性能、发布、持续集成等系统链接到一起,成为团队技术管理的基础。

持续集成

 

搭建系统

架构与基础库

 

知识框架图

image.png

 

 

 

JavaScript(15讲)

关于类型,有哪些你不知道的细节?

什么是运行时类型

运行时类型是代码实际执行过程中我们用到的类型。所有的类型数据都会属于7个类型之一,从变量、参数、返回值到表达式中间结果,任何JavaScript运行过程中产生的数据,都具有运行时类型

类型

JavaScript语言中的每个值都属于某一种数据类型。语言类型广泛用于变量、函数参数、表达式、函数返回值等场合。

  • Undefined
  • Null
  • Boolean
  • String
  • Number
  • Symbol
  • Object

 

Undefined、Null

为什么有的编程规范要求用void0 替代undefined

Undefined类型表示未定义,它的类型只有一个值,就是undefined, 任何变量在赋值前都是Undefined类型,值为undefined, 一般我们可以用全局变量undefined(就是名为undefined的这个变量)来表达这个值,或者void运算来把任意一个表达式变成undefined值。

 

但是呢,因为JavaScript的代码undefined是一个变量,而非是一个关键字,所以,我们为了避免无意中被篡改,建议使用void 0来获取undefined值。(注:这种做法在现代JavaScript开发中并不是普遍使用,因为大部分JavaScript引擎已经解决了undefined被重新赋值的问题 )

 

Null表示的是定义了,但是为空,所以在实际编程中,我们不会把变量赋值为undefined,这样可以保证所有值为undefined的变量,都是从未赋值的自然状态。

Null类型也只有一个值,就是null,它的语义表示空值,与undefined不同,null是JavaScript的关键字,所以在任何代码中,都可以用null关键字来获取null值。

 

Boolean

Boolean类型有两个值,true,false, 它用于表示逻辑意义的真和假,同样有关键字true和false来表示这两个值。

 

String

我们来看看字符串是否有最大长度。

String用于表示文本数据,String有最大长度2^53 -1,这在一般开发中是够用的,但是有趣的是,这个所谓最大长度,并不完全是你理解的字符数。

因为String的意义并非“字符串",而是字符串的UTF16编码,我们字符串的操作,charAt、charCodeAt.length等方法针对的都是UTF16编码,所以,字符串的最大长度,实际上是受字符串的编码长度影响的。

Note: 现行的字符集国际编制,字符是你Unicode的方式表示的,每一个Unicode的码点表示一个字符,理论上,Unicode的范围是无限的。UTF是Unicode的编码方式,规定了码点在计算机中的表示方式。常见的有UTF16和UTF8,Unicode 的码点通常用 U+??? 来表示,其中 ??? 是十六进制的码点值。 0-65536(U+0000 - U+FFFF)的码点被称为基本字符区域(BMP)。

JavaScript中的字符串是永远无法变更。一旦字符串构造出来,无法用任何方式改变字符串内容,所以字符串具有值类型的特征。

举例:

let str = "Hello";
let modifiedStr = str + " World";


console.log(str);           // 输出: "Hello"
console.log(modifiedStr);   // 输出: "Hello World"

在进行拼接操作时,原始的str字符串并没有被修改。相反,拼接操作创建了一个新的字符串"Hello World",并将其赋值给modifiedStr变量。原始的str字符串保持不变,仍然是"Hello"

一旦字符串被创建,就无法直接修改它的内容,任何对字符串的修改操作实际上都是创建了一个新的字符串。因此,在JavaScript中,如果需要对字符串进行修改,我们需要使用新的字符串变量来存储修改后的结果。

 

Number

JavaScript中的Number类型有(2^64 - 2^53 + 3)个值

JavaScript中的Number类型基本符合 IEEE 754-2008规定的双精度浮点数规则,但是JavaScript为了表达几个额外的语言场景,规定了几个例外的情况:

  • NaN,占用了9007199254740990,这原本是符合 IEEE 规则的数字;
  • Infinity: 无穷大
  • -Infinity: 无穷小

值得注意的是,JavaScript中有+0和-0,在加法运算中它们没有什么区别,但是在进行除法运算时,+0和-0的结果会产生不同的无穷大值。

console.log(+0 === -0)
console.log(Object.is(+0, -0))
console.log(1 / +0 === 1 / -0) // false
console.log(1 / +0 === Infinity) // true
console.log(1 / -0 === -Infinity) // true

在 IEEE 754 浮点数标准中,+0 和 -0 使用相同的位模式,只是符号位不同。因此,在比较和相等性判断时,它们被视为相等。

 

 

Symbol

Symbol是ES6中引入的新类型,它是一切非字符串的对象key的集合,在ES6规范中,整个对象系统被用Symbol重塑。

Symbol可以具有字符串类型的描述,但是即使描述相同,Symbol也不相等。

创建Symbol的方式是使用全局的Symbol函数,例如:

var mySymbol = Symbol("my symbol")

一些标准中提到的Symbol,可以在全局的Symbol函数的属性中找到,例如,我们可以使用Symbol.iterator来定义for...of在对象上的行为。

 

 

Object

Object是JavaScript中最复杂的类型,也是JavaScript的核心机制之一。Object表示对象的意思。

在JavaScript中,对象的定义是属性的集合,属性分为数据属性和访问器属性,二者都是key-value结构,key可以是字符串或者Symbol类型。

事实上,JS中的类仅仅是运行时对象的一个私有属性,而JavaScript中是无法自定义类型的。

 

JavaScript中的几个基本类型,都在对象类型中有一个“亲戚”,他们是:

  • Number
  • String
  • Boolean
  • Symbol

所以,我们必须认识到3和new Number(3)是完全不同的值,它们一个时Number类型,一个是对象类型。

Number、String和Boolean,三个构造器时两用的,当跟new搭配时,它们产生对象,当直接调用时,它们表示强制类型转换。

Symbol函数比较特殊,直接用new 调用它会抛出错误,但它仍然是Symbol对象的构造器。

 

JavaScript语言设计上视图模糊对象和基本类型之间的关系,我们可以把对象方法在基本类型上使用,比如:

console.log("abc".charAt(0))

原因:

.运算符提供了装箱操作,它会根据基础类型构造一个临时对象,使得我们能在基础类型上调用对象的方法。

Object.prototype.toSrring(),这个原型对象上的toString方法返回一个表示该对象的字符串,“[object type]”, 其中type是对象的类型,例如var o = new Object() o.toString() 会返回[object Object] ,按道理,这个toString方法只能用于对象类型的数据,但是发现其他数据类型也可以用,例如123.toString(),

这是因为其内部发生了装箱转换,使用new Number(123)生成对象后调用了toString方法,如果我们改变toString中this的指向的数据,那就可以识别任何数据类型了。

 

 

类型转换

因为JS是弱语言类型,所以类型转换发生非常频繁,大部分我们熟悉的运算都先会进行类型转换。大部分类型转换符合人类的直觉,但是如果我们不起理解类型转换的严格定义,很容易造成一些代码中的判断失误。

 

NumberToString

在较小的范围内,数字到字符串的转换是完全符合你直觉的十进制表示。当Number绝对值较大或较小时,字符串表示则是使用科学计数法来表示的。

 

装箱操作

每一种基本类型Number、String、Boolean、Symbol在对象中都有对应的类,所谓装箱转换,正是把基本类型转为对应的对象,它是类型转换中一种相当重要的种类。

 

装箱机制会频繁产生临时对象,在一些对性能要求较高的场景下,我们应该尽量避免对基本类型做装箱操作。

 

拆箱操作

在JS中,规定了ToPrimitive函数,它是对象类型到基本类型的转换,即拆箱操作。

对象到String和Number的转换都遵循“先拆箱再转化”的规则,通过拆箱转换,把对象变成基本类型,再从基本类型转为对应的String或者Number。

 

面向对象还是基于对象?

JS标准对基于对象的定义:语言和宿主的基础设施有对象来提供,并且JS程序即是一系列互相通讯的对象集合。

 

什么是面向对象?

在英文中,对象是一切事物的总称,这和面向对象编程的抽象思维有互通之处。

对象并不是计算机领域凭空造出来的概念,它是顺着人类思维模式产生的一种抽象(于是面向对象编程也被认为是:更接近人类思维模式的一种编程范式)

 

对象究竟是什么?

对象这一概念在人类幼儿期形成,在幼年期,我们总是先认识到某一个苹果能吃(也就是对象),继而认识到所有的苹果都可以吃(所有苹果;类),再到后来我们才能意识到三个苹果和三个梨之间的联系,进而产生数字3的概念

在《面向对象分析与设计》这本书上,从人类的认知角度来说对象应该是下列事物之一:

  • 一个可以触摸或者看得的的东西
  • 人的智力可以理解的东西
  • 可以指导思考或行动(进行想象或施加动作)的东西

 

Java中是使用类的方式来描述对象。

而JavaScript早年却选择了一个更为冷门的方式:原型

 

JavaScript对象的特征:

对象有如下几个特点:

  • 对象具有唯一标识:即使完全相同的两个对象,也并非同一个对象
  • 对象有状态:对象具有状态,同一个对象可能处于不同状态之下
  • 对象具有行为:即对象的状态,可能因为它的行为产生变迁

 

第一个特征,对象具有标识性,一般而言,各种语言的的对象唯一标识性都是用内存地址来体现的,对象具有唯一标识的地址,所以具有唯一的标识。

 

关于对象的第二个和第三个特征:状态和行为,Java中则称它们为属性和方法

 

在JavaScript中,将状态和行为统一抽象为“属性”,考虑到JavaScript中将函数设计成一种特殊的对象,所以,JavaScript中的行为和状态都能用属性来抽象。

例子

var o = {
  d: 1,
  f() {
    console.log(this.d)
  }
}

上述代码展示了普通属性和函数作为属性的一个例子,其中o是对象,d是一个属性,而函数f也是一个属性。

在实现了对象基本特征的基础上,我认为,JavaScript中对象独有的特色是:对象具有高度的动态性,这是因为JavaScript赋予了使用者在运行时为对象添改状态和行为的能力。

例子:

JavaScript运行时允许向对象添加属性,这就跟绝大多数基于类的、静态的对象设计完全不同。

// 创建一个空对象
var person = {};
// 向对象添加属性
person.name = "John";
person.age = 30;
// 在运行时添加属性
var propertyName = "address";
var propertyValue = "123 Main St";
person[propertyName] = propertyValue;

 

为了提高抽象能力,JavaScript的属性被设计成比别的语言更加复杂的形式,它提供了数据属性和访问器属性(getter/setter)两类。

 

JavaScript对象的两类属性

对JavaScript来说,属性并非只是简单的名称和值,JavaScript用一组特征来描述属性。

第一类属性: 数据属性

  • value: 属性的值
  • writable: 决定属性能否被赋值
  • enumerable: 决定for in 能否枚举该属性
  • configurable: 决定该属性能否被删除或者改变特征值

第二类属性: 访问器(getter和setter)属性

  • getter: 函数或者undefined, 在取属性值时被调用
  • setter: 函数或者undefined, 在设置属性值时被调用
  • enumerable: 决定for in 能否枚举该属性
  • configurable: 决定该顺序能否被删除或者改变特征值

 

访问器属性使得属性在读和写时执行代码,它允许使用者在读和写属性时,可以视为一种函数的语法糖。

我们通常用于定义属性的会产生数据属性,其中的wraitable、enumerable、configurable都默认为true, 我们可以使用内置函数getOwnPropertyDescriptor来查看。

var o = {a : 1}
o.b = 2
Object.getOwnPropertyDescriptor(o, "a") // {value: 1, writable: true, enumerable: true, configurable: true}
Object.getOwnPropertyDescriptor(o, "b")

如果我们想要改变属性的特征,或者定义访问器属性,我们可以使用Object.defineProperty,示例如下

var o = { a: 1 }; 
Object.defineProperty(o, "b", {value: 2, writable: false, enumerable: false, configurable: true}); //a和b都是数据属性,但特征值变化了 
Object.getOwnPropertyDescriptor(o,"a"); // {value: 1, writable: true, enumerable: true, configurable: true} Object.getOwnPropertyDescriptor(o,"b"); // {value: 2, writable: false, enumerable: false, configurable: true} 
o.b = 3; 
console.log(o.b); // 2

在创建对象时,也可以使用get和set关键字来创建访问器属性

var o = {get a() {return 1}}
console.log(o.a) // 1

访问器属性和数据属性不同,每次访问属性都会执行getter或者setter函数,这里我们的getter函数返回了1,所以o.a每次都得到1

 

实际上,JavaScript对象的运行时是一个“属性的集合”,属性以字符串或者Symbol为key, 以数据属性特征值或者访问器属性特征值为value.

 

对象是一个属性的索引结构。

 

 

我们真的需要模拟类吗?

早期的JavaScript程序员一般都有过使用JavaScript“模拟面向对象”的经历。

JavaScript本身就是面向对象的,它并不需要模拟,只是它实现面向对象的方式和主流的流派不太一样。

这些“模拟面向对象”,实际上做的事情就是“模拟基于类的面向对象”,

从ES6开始,JavaScript提供了class关键字来定义类,尽管,这样的方案仍然是基于原型运行时系统的模拟,但是它修正了之前的一些常见的“坑”,统一了社区的方案。

实际上,我认为“基于;类”并非面向对象的唯一形态,如果我们把视线从“类”移开, Brendan当年选择的原型系统,就是一个非常优秀的抽象对象的形式。

 

什么是原型?

原型是顺应人类自然思维的产物, 有个成语叫“照猫画虎”, 这里的猫看起来就是虎的原型,所以,由此我们可以看出,用原型来描述对象的方法可以说是古已有之。

 

在不同的编程语言中,设计者也利用各种不同的语言特性来抽象描述对象。

最为成功的流派是使用“类”的方式来描述对象,例如Java、C++

“基于类”的编程提倡使用一个关注分类和类之间关系开发模型,在这类语言中,总是先有类,再从类去实例化一个对象,类与类之间又可能会形成继承、组合等关系。类又往往与语言的类型系统整合,形成一定编译时的能力。

 

由此相对,“基于原型”的编程看起来更为提倡程序员去关注一系列对象实例的行为,而后才去关心如何将这些对象,划分到最近的使用方式相似的原型对象,而不是将它们分成类。

 

基于原型的面向对象系统通过“复制”的方式来创建新对象,一些语言的实现中,还允许复制一个空对象,这实际上就是创建一全新的对象。

 

基于原型和基于类都能够满足基本的复用和抽象的需求,但是适用的场景不太相同。

 

原型系统的“复制操作”有两种实现思路:

  • 一个是并不真的去复制一个原型对象,而是使得新对象持有一个原型的引用。
  • 另一个是切实的复制对象,从此两个对象再无关联

 

 

JavaScript的原型

 

如果我们抛开JavaScript用于模拟Java类的复杂语法设施(如new Function Object、函数的prototype属性等), 原型系统可以说相当简单。

  • 如果所有对象都有私有字段[prototype],就是对象的原型。
  • 读一个属性,如果对象本身没有,则会继续访问对象的原型,直到原型为空或者找到为止。没有找到返回undefined

 

这个模型在ES的各个历史版本中并没有很大改变,但从ES6以来, JavaScript提供了一系列内置函数,以便更为直接地访问操纵原型,三个方法分别是:

  • Object.create: 根据指定的原型创建一个新对象,原型可以是null
  • Object.getPropertyOf: 获取一个对象的原型
  • Object.setPropertyOf: 设置一个对象的原型

 

利用这三个方法,我们可以完全抛开类的思维,利用原型来实现抽象和复用。

 

我们仍要把new理解成JavaScript面向对象的部分,下面就来讲一下new操作具体做了哪些事情?

new运算接受一个构造器和一组调用参数,实际上做了几件事:

  • 以构造器的prototype属性为原型,创建新对象
  • 将this和调用参数传给构造器,执行
  • 如果构造器返回的是对象,则返回,否则返回第一个创建的对象。

它客观上提供了两种方式,一种是在构造器中添加属性,二是在构造器的prototype属性上添加属性。

 

没有 Object.create、Object.setPrototypeOf 的早期版本中,new 运算是唯一一个可以指定[[prototype]]的方法(当时的 mozilla 提供了私有属性 proto,但是多数环境并不支持),所以,当时已经有人试图用它来代替后来的 Object.create.

 

 

ES6中的类

ES6加入了新特性class, 在任何场景,我都推荐使用ES6的语法来定义类,而令function回归原本的函数定义。

ES6中引入了class关键字,并且在标准中删除了[[class]]相关的私有属性描述,类的概念正式从属性升级成语言的基础设施,从此,基于类的编程方式成为了JavaScript的官方编程范式。

类的基本写法:

class Rectangle {
    constructor(height, width) {
        this.height = height
        this.width = width
    }
    // getter
    get area() {
        return this.calcArea()
    }
    calcArea() {
        return this.height * this.width
    }
}

我们通过get/set关键字来创建getter/setter, 通过括号和大括号创建方法,数据型成员最好写在构造器里面。

类的写法实际上也是由原型运行时来继承的,逻辑上JavaScript认为每个类是有共同原型的一组对象,类中定义的方法和属性则会被写在原型对象之上。

类提供了继承能力:

// 类的继承
class Animal {
    constructor(name) {
        this.name = name
    }
    speak() {
        console.log(this.name + 'makes a noise')
    }
}


class Dog extends Animal {
    constructor(name) {
        super(name)
    }
    speak() {
        console.log(this.name + 'barks')
    }
}
let dog = new Dog('小黄')
dog.speak()

以上代码创造了Animal类,并且通过extends关键字让Dog继承了它,展示了最终调用子类speak方法获取了父类的name

 

比起早期的原型模拟方式,使用extends关键字自动设置了constructot,并且会自动调用父类的钩子函数。

 

所以当我们使用类的思想来设计代码时,应该尽量使用class来声明类,而不是用旧语法,拿函数来模拟对象。

 

 

JavaScript中对象的分类

我们可以把对象分成几类:

  • 宿主对象: 由JavaScript宿主环境提供的对象,它的行为完全由宿主环境决定
  • 内置对象: 由JavaScript语言提供的对象
    • 固有对象: 由标准规定,随着JavaScript运行时创建而自动创建的实例对象
    • 原生对象: 可以由用户通过Array、RegExp等内置构造器或者特殊语法创建的对象
    • 普通对象: 由{}语法、Object构造器或者class关键字定义类创建的对象,它能够被原型继承。

 

宿主对象

JavaScript宿主对象千奇百怪,但是前端最熟悉的无疑是浏览器环境中的宿主了。

在浏览器环境中,我们都知道全局对象是window,window上又有很多属性

JavaScript标准中规定了全局对象属性,W3C的各种标准中规定了Window对象的其他属性。

 

内置对象-固有对象

固有对象在任何JavaScript代码执行前就已经被创建出来了,它们通常扮演类似基础库的角色。

 

内置对象-原生对象

我们把JavaScript中,能够通过语言本身的构造器创建的对象称为原生对象。在JavaScript标准中,提供了30多个构造器。

image.png 通过这些构造器,我么可以用new运算创建新的对象,所以我们把这些对象称为原生对象。

几乎所有这些构造器的能力都是无法用纯JavaScript代码实现的,它们也无法用class/extend语法继承。

用对象来模拟函数与构造器:函数对象与构造器对象

函数对象的定义是: 具有[[call]]私有字段的对象,构造器对象的定义:具有私有字段[[construct]]的对象

 

任何对象只需要实现[[call]],它就是一个函数对象,可以去作为函数被调用。而如果它能实现[[construct]],它就是一个构造器对象,可以作为构造器被调用。

 

Promise里的代码为什么比setTimeout先执行

当拿到一段JavaScript代码时,浏览器或者Node环境首先要做的就是: 传递给JavaScript引擎,并且要求它去执行。

然而,执行JavaScript并非一锤子买卖,宿主环境当遇到一些事情时,会继续把一段代码传递给JavaScript引擎去执行,此外,我们可能还会提供API给JavaScript引擎,比如setTimepout这样的API, 它允许JavaScript在特定的时机执行。

 

所以,我们首先应该形成一个感性的认知: 一个JavaScript引擎会常驻于内存中,它等待着宿主把JavaScript代码或者函数传递给它执行。

 

在 ES3 和更早的版本中,JavaScript 本身还没有异步执行代码的能力,这也就意味着,宿主环境传递给 JavaScript 引擎一段代码,引擎就把代码直接顺次执行了,这个任务也就是宿主发起的任务。但是,在 ES5 之后,JavaScript 引入了 Promise,这样,不需要浏览器的安排,JavaScript 引擎本身也可以发起任务了。由于我们这里主要讲 JavaScript 语言,那么采纳 JSC 引擎的术语,我们把宿主发起的任务称为宏观任务,把 JavaScript 引擎发起的任务称为微观任务。

 

宏观和微观任务

JavaScript引擎等待宿主环境分配宏观任务,在操作系统中,通常等待的行为都是一个事件循环,所以,在Node术语中,也会把这个部分称为事件循环。

 

每次的执行过程,其实就是一个宏观任务。我们可以大概理解:宏观任务的队列就相当于事件循环。

 

在宏观任务中,JavaScript的Promise还会产生异步代码,JavaScript必须保证这些异步代码在一个宏观任务中完成,因此,每个宏观任务中又包含了一个微观任务队列。

image.png

 

有了宏观任务和微观任务机制,我们就可以实现JavaScript引擎级和宿主级的任务了,如Promise永远在队列尾部添加微观任务。setTimeout等宿主API,则会添加宏观任务。

 

闭包和执行上下文到底是怎么回事?

 

image.png 闭包

闭包在编程语言领域,它表示一种函数。

理解:

闭包其实只是绑定了执行环境的函数,这个函数并不是印在书本里的一条简单的表达式,闭包与普通函数的区别是,它携带了执行的环境。

闭包定义中,闭包包含两个部分:

  • 环境部分
    • 环境
    • 标识符列表
  • 表达式部分

 

当我们把视角放在JavaScript的标准中,我们发现,标准中并没有出现过closure这个术语,但是,我们可以在JavaScript中找到对应的闭包组成部分。

 

  • 环境部分
    • 环境: 函数的词法环境(执行上下文的一部分)
    • 标识符列表: 函数中用到的未声明的变量
  • 表达式部分: 函数体

 

我们可以认为,JavaScript中的函数完全符合闭包的定义。它的环境部分是函数的词法环境,它的标识符列表是函数中用到的未声明变量,它的表达式部分就是函数体。

这里我们容易产生一个场景的概念误区,有些人会把JavaScript执行上下文,或者作用域这个概念当做闭包。

 

实际上JavaScript中跟闭包对应的概念就是“函数”, 可能是这个概念太过于普通,跟闭包看起来又没有什么联系。

 

执行上下文:执行的基础设施

相比普通函数,JavaScript函数的主要复杂性来自于它携带的“环境部分”。当然,发展到今天的JavaScript,它所定义的环境部分,已经比当初经典的定义复杂了很多。

 

JavaScript中与闭包“环境部分”相对应的术语是“词法环境”, 词法环境只是JavaScript执行上下文的一部分。

 

理解:

在JS中,函数其实就是闭包,不管该函数内部是否使用外部变量,它都是一个闭包, 如闭包定义的那样,由环境和表达式组成,作为js函数, 环境为词法环境, 而表达式为函数本身。而词法环境是执行上下文的一部分,执行上下文包括this绑定、词法环境和变量环境。词法环境是随之执行上下文一起创建的,在函数/脚本/eval执行时创建的

理解闭包,首先需要理解闭包是什么类型的东西,闭包实际上指的是函数,而很多人会把环境、作用域等其他东西当做闭包,是对闭包的概念类型的错误理解。那么如果知道了闭包都是函数,那么什么样的函数是闭包呢,也就是有环境的函数。很显然,在JS 中,任何一个函数都有着自己的环境,这个环境让我们可以去找到定义变量内部的this、外部作用域。

 

很多人认为,要让一个函数能去访问某个应该被回收的内存空间,但由于函数存在对该内存空间的变量的引用而不可回收,这样才形成了闭包。那么这样的话,这样你到底是把这个内存空间当做闭包呢?还是把引用这块内存空间的函数当闭包(以前对闭包的理解就是把引用这块内存空间的函数当做对象,这样是错误的)。假如是前者,则和把环境当做闭包的人犯了同样的错,如果是后者,现在的这个函数实际上和你定义的普通函数本质上没有区别,都含有自己的环境,只不过这个函数的环境多了一些,但本质上没有区别。

 

总结:

普通的JavaScript函数本质上是闭包,它们包含函数体和引用的词法环境。这使得函数能够访问和操作其定义时所处的作用域中的变量和函数。

当函数被定义时,它会捕获所在的词法环境,并且可以访问该环境中的变量和函数,这意味着函数本身就是一个封装了函数体和引用环境的闭包。