Skip to content

Commit 7cb3193

Browse files
author
zhangdong
committed
feat: 初始化
1 parent 6998eea commit 7cb3193

File tree

4 files changed

+280
-0
lines changed

4 files changed

+280
-0
lines changed

.vitepress/config.mts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,18 @@ export default defineConfig({
9898
text: "Rest与Spread",
9999
link: "/javascript/grammar/restSpread",
100100
},
101+
{
102+
text: "闭包",
103+
link: "/javascript/grammar/closure",
104+
},
105+
{
106+
text: "来自旧时代的var",
107+
link: "/javascript/grammar/var",
108+
},
109+
{
110+
text: "杂项",
111+
link: "/javascript/grammar/misc",
112+
},
101113
],
102114
},
103115
],

javascript/grammar/closure.md

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
---
2+
outline: deep
3+
layout: doc
4+
---
5+
闭包是指使用一个特殊的属性 [[Environment]] 来记录函数自身的创建时的环境的函数。它具体指向了函数创建时的词法环境
6+
7+
在 JavaScript 中,所有函数都是天生闭包的(只有一个例外,"new Function")
8+
## 词法环境
9+
在 JavaScript 中,每个运行的函数,代码块 {...} 以及整个脚本,都有一个被称为 **词法环境(Lexical Environment)** 的内部(隐藏)的关联对象。
10+
11+
词法环境对象由两部分组成:
12+
13+
* **环境记录(Environment Record)** —— 一个存储所有局部变量作为其属性(包括一些其他信息,例如 this 的值)的对象。
14+
***外部词法环境** 的引用,与外部代码相关联。
15+
16+
当代码要访问一个变量时 —— 首先会搜索内部词法环境,然后搜索外部环境,然后搜索更外部的环境,以此类推,直到全局词法环境。
17+
18+
:::tip :rocket:
19+
所有的函数在“诞生”时都会记住创建它们的词法环境。从技术上讲,这里没有什么魔法:所有函数都有名为 [[Environment]] 的隐藏属性,该属性保存了对创建该函数的词法环境的引用。
20+
21+
函数记住它创建于何处的方式,与函数被在哪儿调用无关
22+
:::
23+
24+
## 内存泄漏
25+
通常,函数调用完成后,会将词法环境和其中的所有变量从内存中删除。因为现在没有任何对它们的引用了。与 JavaScript 中的任何其他对象一样,词法环境仅在可达时才会被保留在内存中。
26+
27+
但是,如果有一个嵌套的函数在函数结束后仍可达,则它将具有引用词法环境的 [[Environment]] 属性。
28+
29+
在下面这个例子中,即使在(外部)函数执行完成后,它的词法环境仍然可达。因此,此词法环境仍然有效。
30+
31+
例如:
32+
```js
33+
function f() {
34+
let value = 123;
35+
36+
return function() {
37+
alert(value);
38+
}
39+
}
40+
41+
let g = f(); // g.[[Environment]] 存储了对相应 f() 调用的词法环境的引用
42+
```
43+
请注意,如果多次调用 f(),并且返回的函数被保存,那么所有相应的词法环境对象也会保留在内存中。下面代码中有三个这样的函数:
44+
```js
45+
function f() {
46+
let value = Math.random();
47+
48+
return function() { alert(value); };
49+
}
50+
51+
// 数组中的 3 个函数,每个都与来自对应的 f() 的词法环境相关联
52+
let arr = [f(), f(), f()];
53+
```
54+
当词法环境对象变得不可达时,它就会死去(就像其他任何对象一样)。换句话说,它仅在至少有一个嵌套函数引用它时才存在。这会导致内存泄漏

javascript/grammar/misc.md

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
---
2+
outline: deep
3+
layout: doc
4+
---
5+
函数继承与对象,所以函数上也能添加属性
6+
```js
7+
function sayHi() {
8+
alert("Hi");
9+
10+
// 计算调用次数
11+
sayHi.counter++;
12+
}
13+
sayHi.counter = 0; // 初始值
14+
15+
sayHi(); // Hi
16+
sayHi(); // Hi
17+
18+
alert( `Called ${sayHi.counter} times` ); // Called 2 times
19+
```
20+
21+
函数属性有时会用来替代闭包
22+
23+
两者最大的不同就是如果 count 的值位于外层(函数)变量中,那么外部的代码无法访问到它,只有嵌套的那些函数可以修改它。也就是透明的
24+
25+
## 命名函数表达式(NFE)
26+
```js
27+
let sayHi = function func(who) {
28+
alert(`Hello, ${who}`);
29+
};
30+
```
31+
我们这里得到了什么吗?为它添加一个 "func" 名字的目的是什么?
32+
33+
首先请注意,它仍然是一个函数表达式。在 function 后面加一个名字 "func" 没有使它成为一个函数声明,因为它仍然是作为赋值表达式中的一部分被创建的。
34+
35+
添加这个名字当然也没有打破任何东西。
36+
37+
关于 func 有两个特殊的地方,这就是添加它的原因:
38+
39+
* 它允许函数在内部引用自己。
40+
* 它在函数外是不可见的。
41+
42+
```js
43+
let sayHi = function func(who) {
44+
if (who) {
45+
alert(`Hello, ${who}`);
46+
} else {
47+
func("Guest"); // 使用 func 再次调用函数自身
48+
}
49+
};
50+
51+
sayHi(); // Hello, Guest
52+
53+
// 但这不工作:
54+
func(); // Error, func is not defined(在函数外不可见)
55+
```
56+
57+
我们为什么使用 func 呢?为什么不直接使用 sayHi 进行嵌套调用?
58+
59+
当然,在大多数情况下我们可以这样做:
60+
61+
```js
62+
let sayHi = function(who) {
63+
if (who) {
64+
alert(`Hello, ${who}`);
65+
} else {
66+
sayHi("Guest");
67+
}
68+
};
69+
```
70+
上面这段代码的问题在于 sayHi 的值可能会被函数外部的代码改变。如果该函数被赋值给另外一个变量(译注:也就是原变量被修改),那么函数就会开始报错:
71+
```js
72+
let sayHi = function(who) {
73+
if (who) {
74+
alert(`Hello, ${who}`);
75+
} else {
76+
sayHi("Guest"); // Error: sayHi is not a function
77+
}
78+
};
79+
80+
let welcome = sayHi;
81+
sayHi = null;
82+
83+
welcome(); // Error,嵌套调用 sayHi 不再有效!
84+
```
85+
发生这种情况是因为该函数从它的外部词法环境获取 sayHi。没有局部的 sayHi 了,所以使用外部变量。而当调用时,外部的 sayHi 是 null。
86+
87+
我们给函数表达式添加的可选的名字,正是用来解决这类问题的。
88+
## "new Function" 语法
89+
还有一种创建函数的方法
90+
91+
```js
92+
let func = new Function ([arg1, arg2, ...argN], functionBody);
93+
```
94+
95+
这是一个带有两个参数的函数:
96+
```js
97+
let sum = new Function('a', 'b', 'return a + b');
98+
99+
alert( sum(1, 2) ); // 3
100+
```
101+
这里有一个没有参数的函数,只有函数体:
102+
```js
103+
let sayHi = new Function('alert("Hello")');
104+
105+
sayHi(); // Hello
106+
```
107+
new Function 允许我们将任意字符串变为函数。例如,我们可以从服务器接收一个新的函数并执行它
108+
109+
new Function 创建一个函数,那么该函数的 [[Environment]] 并不指向当前的词法环境,而是指向全局环境。
110+
111+
因此,此类函数无法访问外部(outer)变量,只能访问全局变量。
112+
```js
113+
function getFunc() {
114+
let value = "test";
115+
116+
let func = new Function('alert(value)');
117+
118+
return func;
119+
}
120+
121+
getFunc()(); // error: value is not defined
122+
```
123+
将其与常规行为进行比较:
124+
```js
125+
function getFunc() {
126+
let value = "test";
127+
128+
let func = function() { alert(value); };
129+
130+
return func;
131+
}
132+
133+
getFunc()(); // "test",从 getFunc 的词法环境中获取的
134+
```
135+
136+
:::tip :thinking: 这种特性看起来有点奇怪,不过在实际中却非常实用
137+
在将 JavaScript 发布到生产环境之前,需要使用 压缩程序(minifier) 对其进行压缩 —— 一个特殊的程序,通过删除多余的注释和空格等压缩代码 —— 更重要的是,将局部变量命名为较短的变量。
138+
139+
例如,如果一个函数有 let userName,压缩程序会把它替换为 let a(如果 a 已被占用了,那就使用其他字符),剩余的局部变量也会被进行类似的替换。一般来说这样的替换是安全的,毕竟这些变量是函数内的局部变量,函数外的任何东西都无法访问它。在函数内部,压缩程序会替换所有使用了这些变量的代码。压缩程序很聪明,它会分析代码的结构,而不是呆板地查找然后替换,因此它不会“破坏”你的程序。
140+
141+
但是在这种情况下,如果使 new Function 可以访问自身函数以外的变量,它也很有可能无法找到重命名的 userName,这是因为新函数的创建发生在代码压缩以后,变量名已经被替换了。
142+
143+
即使我们可以在 new Function 中访问外部词法环境,我们也会受挫于压缩程序。
144+
145+
此外,这样的代码在架构上很差并且容易出错。
146+
147+
当我们需要向 new Function 创建出的新函数传递数据时,我们必须显式地通过参数进行传递。
148+
:::

javascript/grammar/var.md

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
---
2+
outline: deep
3+
layout: doc
4+
---
5+
* “var” 声明的变量只有函数作用域和全局作用域,没有块级作用域
6+
* “var” 允许重新声明
7+
* 声明会被提升,但是赋值不会(函数声明也会自动提升,函数表达式不会)
8+
* 在浏览器中,使用 var(而不是 let/const!)声明的全局函数和变量会成为全局对象的属性。
9+
```js
10+
function sayHi() {
11+
alert(phrase);
12+
13+
var phrase = "Hello";
14+
}
15+
16+
sayHi();
17+
```
18+
声明在函数刚开始执行的时候(“提升”)就被处理了,但是赋值操作始终是在它出现的地方才起作用。所以这段代码实际上是这样工作的:
19+
```js
20+
function sayHi() {
21+
var phrase; // 在函数刚开始时进行变量声明
22+
23+
alert(phrase); // undefined
24+
25+
phrase = "Hello"; // ……赋值 — 当程序执行到这一行时。
26+
}
27+
28+
sayHi();
29+
```
30+
31+
## IIFE
32+
在之前,JavaScript 中只有 var 这一种声明变量的方式,并且这种方式声明的变量没有块级作用域,程序员们就发明了一种模仿块级作用域的方法。这种方法被称为“立即调用函数表达式”(immediately-invoked function expressions,IIFE)。
33+
```js
34+
(function() {
35+
36+
var message = "Hello";
37+
38+
alert(message); // Hello
39+
40+
})();
41+
```
42+
43+
## 全局对象
44+
全局对象提供可在任何地方使用的变量和函数。默认情况下,这些全局变量内建于语言或环境中。
45+
46+
在浏览器中,它的名字是 “window”,对 Node.js 而言,它的名字是 “global”,其它环境可能用的是别的名字。
47+
48+
最近,globalThis 被作为全局对象的标准名称加入到了 JavaScript 中,所有环境都应该支持该名称。所有主流浏览器都支持它。
49+
50+
假设我们的环境是浏览器,我们将在这儿使用 “window”。如果你的脚本可能会用来在其他环境中运行,则最好使用 globalThis。
51+
52+
全局对象的所有属性都可以被直接访问:
53+
```js
54+
alert("Hello");
55+
// 等同于
56+
window.alert("Hello");
57+
```
58+
59+
## 使用 polyfills
60+
我们使用全局对象来测试对现代语言功能的支持。
61+
62+
例如,测试是否存在内建的 Promise 对象(在版本特别旧的浏览器中不存在):
63+
64+
if (!window.Promise) {
65+
alert("Your browser is really old!");
66+
}

0 commit comments

Comments
 (0)