Cirry's Blog

《你不知道的Javascript》上卷第一部分(总结)

2024-02-28
阅读
最后更新:2024-06-20
13分钟
2583字

很早以前我就购买了这本书,也看过几页,却一直没有静下心来去好好拜读,这次静下心来阅读并记录一下吧。

eval、with

关于eval这个关键字,都知道是用来转换字符串为代码的,而且因为安全问题几乎没有人使用它。

这次在看作用域的相关内容的时候,又了解到不仅仅有安全问题,还有作用域问题和优化问题,同样更少使用到的with关键字也有相似的问题。

1
function foo(str, a) {
2
eval(str) // 作用域欺骗
3
console.log(a, b)
4
}
5
6
var b = 2;
7
foo("var b = 3;", 1) // 1,3

with 它的作用是在一个代码块中创建一个新的作用域,并将指定的对象添加到这个作用域中。这样,你就可以在代码块中直接访问该对象的属性,而不需要每次都使用对象的名称。

with 关键字的作用一:

1
var person = {
2
name: "John",
3
age: 30
4
};
5
6
// 使用 with 关键字访问 person 对象的属性
7
with (person) {
8
console.log(name); // 直接访问 name 属性,而不需要使用 person.name
9
console.log(age); // 直接访问 age 属性,而不需要使用 person.age
10
}

with可以将一个没有或者多个属性的对象处理为一个完全隔离的词法作用域:

1
function foo(obj) {
2
with (obj) {
3
a = 2;
4
}
5
}
6
7
var o1 = {
8
a: 3
9
}
10
var o2 = {
11
b: 3
12
}
13
foo(o1);
14
console.log(o1.a) // 2
15
foo(o2);
2 collapsed lines
16
console.log(o2.a) // 没有o2.a这个属性所以结果是 undefined
17
console.log(a) // 2, 副作用a被泄露到全局了,因为没有a属性,就在作用域中创建了一个变量a

导致的性能问题:

从上面的示例中可以看出eval和with都可以在代码执行阶段改变词法作用域,因此js引擎在编译阶段对词法的静态分析和性能优化,都可能会因为eval和with变得无意义。

自执行函数

1
//我们把一个匿名函数赋值给了foo
2
var foo = function () {
3
console.log(1)
4
}
5
6
foo() // 执行这个函数,输出1
7
8
// 那如果直接在控制台中打印foo,就会返回一个function
9
10
> foo
11
< ƒ(){console.log(1)}
12
13
// 我们可以省略赋值给foo的操作,用这个function来替代foo
14
// foo() ==> function(){console.log(1)}()
15
// 如果直接这样写的话,代码就被拆开了,所有用个括号把这个函数包裹起来,记住这个无法执行的表达式
10 collapsed lines
16
17
// 我们应该把这个函数视作一个整体,用()包裹一下,这就变成了一个函数表达式
18
(function () {
19
console.log(1)
20
})()
21
22
// 如果需要传递参数的写法
23
(function (x, y) {
24
console.log(x + y)
25
})(1, 2)

这样写的好处有两点:

  • 避免创建任何新变量去污染作用域
  • 不需要函数名,并且能够自动运行,函数名也可能会污染所在作用域

解释一下,两个()的作用,其实第一个括号不能简单的理解为上面注释中提到的只是为了包裹起来,而是为了把一个匿名/具名函数变成一个函数表达式。第二个括号是为了直接调用这个函数。

这里需要理解一个知识点就是函数表达式函数声明

区分函数声明和表达式最简单的方法是看function关键字出现在声明中的位置(不仅仅是一行代码,而是整个声明中的位置)。如果function是声明中的第一个词,那么就是一个函数声明,否则就是一个函数表达式。

比如:

1
// 函数声明
2
function foo() {
3
}
4
5
// 函数表达式
6
var bar = function () {
7
}

再来理解一下通俗的表达式的含义:

表达式是一组代码的集合,可以返回一个值。 语句是都有一个结果值的。

每一个合法的表达式都能计算成某个值,但从概念上讲,有两种类型的表达式:有副作用的(比如赋值)和单纯计算求值的。

表达式 x=7 是第一类型的一个例子。该表达式使用=运算符将值 7 赋予变量 x。这个表达式自己的值等于 7。

代码 3 + 4 是第二个表达式类型的一个例子。该表达式使用 + 运算符把 3 和 4 加到一起但并没有把结果(7)赋值给一个变量。

1
var result = 2 * 2; // result = 4
2
3
var myArray = [1, 2, 3];
4
5
var bar = function () {
6
}; // bar = f (){}
7
8
(function () {
9
}) // 这也是一个表达式

所以注意上面的自执行函数不是函数声明,而是一个函数表达式。 函数声明是必须要提供函数名的,而函数表达式可以省略函数名。

default

不过用具名函数声明也可以是自执行函数:

1
(function bar(x, y) {
2
console.log(x + y)
3
})(1, 2) // 3

还记得上面那个无法执行的表达式的吗?如果你再给它包裹一层(),它就又可以正常执行了,这就是自执行函数的另一种写法。

1
(function () {
2
console.log(1)
3
}()) // 1

解释一下为什么之前那么写不行,在外面加个大括号又可以了:

function(){console.log(1)}()无法执行在控制台中会直接报错Uncaught SyntaxError: Function statements require a function name,因为这被认为是一个函数声明,而函数声明必须要提供函数名。

那我们给他一个函数名,前面说了有函数名也可以写成自执行函数。

function bar(){console.log(1)}(),控制台继续报错Uncaught SyntaxError: Unexpected token ')',说我们的语法有错误,因为函数声明是无法声明后直接跟()去执行的,而函数表达式可以。

那接下来我们给它的最外层套一个()

(function bar(){console.log(1)}()),控制台正常输出1,这样写解析器会直接把function bar(){console.log(1)}()视作函数表达式,而不是上面的函数声明后直接执行。

var、let

let 不会进行变量提升。

1
{
2
console.log(bar)
3
let bar = 2; // VM744:2 Uncaught ReferenceError: Cannot access 'bar' before initialization
4
}
1
{
2
console.log(bar)
3
var bar = 2; // undefined
4
}

let不会扩大作用域范围。

1
for (let i = 0; i < 5; i++) {
2
console.log(i);
3
} // 1,2,3,4,5
4
5
console.log(i); // Uncaught ReferenceError: i is not defined
1
for (var i = 0; i < 5; i++) {
2
console.log(i);
3
} // 1,2,3,4,5
4
5
console.log(i); // 5

let会生成一个块级作用域,而var会生成全局作用域。后面你就会看到,let和var在这方面的区别。

提升

无论这个表达式var a = 2写在什么地方,都会编译阶段拆开为两个步骤:变量声明var a;, 变量赋值a = 2,函数声明和变量声明同理,函数表达式不会被提升。

看个例子:

1
console.log(a); // undefined
2
foo(); // 1
3
bar(); // TypeError
4
5
// ... 省略100行代码
6
7
var a = 2
8
9
function foo() {
10
console.log(1)
11
}
12
13
var bar = function () {
14
console.log(2)
15
}

它会被解析为:

1
function foo() { // 函数foo的声明,函数声明优于变量声明
2
console.log(1)
3
}
4
5
var a // 变量a的声明
6
var bar // 变量bar的声明
7
8
console.log(a); // undefined
9
foo(); // 1
10
bar(); // TypeError,引用错误,这个时候的bar还是undefined
11
// ...省略100行代码
12
13
a = 2
14
bar = function () {
15
console.log(2)
1 collapsed line
16
}

闭包

还记得我几年前刚接触前端的时候,第一次看到闭包的时候,我也很难理解它,后来我总结了一句话:闭包就是一个函数里面返回一个函数,返回的函数里用了父函数的变量

在某一种形式上,闭包就是这样。

1
function foo() {
2
var a = 2;
3
return function bar() {
4
console.log(a)
5
}
6
}
7
8
var baz = foo()
9
baz(); // 2

一个变量或者一个函数在执行结束之后会自然地被回收,而闭包中因为内部作用域被外部引用了就导致无法被回收,而这个引用就叫做闭包。

而实际上,无论通过何种手段将内部函数传递到所在的词法作用域以外,它都会持有对原始定义作用域的引用,无论在何处执行这个函数都会使用闭包。

闭包强调的其实一种引用关系,而不是具体的某个形式。

1
function wait(message) {
2
setTimeout(function timer() {
3
console.log(message)
4
}, 1000)
5
}
6
7
wait("hello world!")

像上面这样的,内部函数timer作为setTimeout的回调函数,一直保持对wait函数的作用域引用,可以正常使用message,可以理解为timer函数保有wait函数作用域的闭包。

注意下面这个例子:

1
for (var i = 1; i <= 5; i++) {
2
setTimeout(function timer() {
3
console.log(i)
4
}, i * 1000)
5
}

这是一个循环五次,每秒打印一个i的函数,我们期望的值应该是1,2,3,4,5,但是实际打印出来的结果是6,6,6,6,6。

为什么会这样?

for是一个同步函数,setTimeout是一个异步函数,实际上异步函数会在同步函数执行完后才执行,所以当for循环执行完后,i的值已经变成了6了。

那为什么又都是6,代码上,每次给setTimeout赋值了一个i,第一次循环的时候,传入进去的i应该是1,第二次是2,为什么又都是6?

所有的setTimeout都是在for循环中的,而这个for循环的作用域中只有一个i,这五个setTimeout函数引用的其实是同一个i,当for函数运行完之后,i只有一个值就是6。

这里就体现到了闭包的作用了,隔离作用域:

1
for (var i = 1; i <= 5; i++) {
2
(function (i) {
3
setTimeout(function timer() {
4
console.log(i)
5
}, i * 1000)
6
})(i)
7
}

自执行函数是一个封闭作用域,而且可以传递参数,当i传入到function中的时候,i的值就被封闭在每个迭代内部了。

当然还有一个更优雅的解法就是let,给每一个迭代生成一个块作用域,即每次循环的时候都会声明一个i变量并使用上一次迭代的值来进行初始化。

1
for (let i = 1; i <= 5; i++) {
2
setTimeout(function timer() {
3
console.log(i)
4
}, i * 1000)
5
}

上册看完了,目前的看法是不如去看红宝书。

本文标题:《你不知道的Javascript》上卷第一部分(总结)
文章作者:Cirry
发布时间:2024-02-28
版权声明:本作品采用「署名-非商业性使用-相同方式共享 4.0 国际」许可协议进行许可。
感谢大佬送来的咖啡☕
alipayQRCode
wechatQRCode