b
JavaScript
在课程中,除了网络开发,我们还有一个目标和需求,就是学习足够多的JavaScript。
JavaScript在过去的几年里进步很快,在本课程中,我们使用了较新版本的功能。JavaScript标准的官方名称是ECMAScript。目前,最新的版本是2024年6月发布的版本,叫ECMAScript®2024,又叫ES15。
浏览器还不支持JavaScript的所有最新功能。由于这个事实,很多在浏览器中运行的代码都是从较新版本的JavaScript转译成较旧的、更兼容的版本。
如今,最流行的转译方式是通过Babel。在用Vite创建的React应用中,转译是自动配置的。我们将在本课程的第7章节中仔细研究转译的配置问题。
Node.js是一个基于Google的Chrome V8 JavaScript引擎的JavaScript运行环境,几乎可以在任何地方运行——从服务器到手机应用。让我们练习一下使用Node编写一些JavaScript。最新版本的Node已经能够理解最新版本的JavaScript,所以代码不需要转译。
代码被写入以.js结尾的文件中,通过键入node name_of_file.js命令来运行。
也可以将JavaScript代码写入Node.js控制台,该控制台可以通过在命令行中输入node打开,也可以写入浏览器的开发者工具控制台。Chrome浏览器的最新版本可以很好地处理JavaScript的新功能,无需转译。另外,你可以使用JS Bin这样的工具。
JavaScript在名字和语法上都有点让人想到Java。但是在语言的核心机制上,它们是非常不同的。对于学习过Java的人,尤其是那些没有花时间去研究的JavaScript特性的人,可能会对JavaScript的行为感到有点陌生。
在某些圈子里,还流行尝试用JavaScript“模拟”Java的特性和设计模式。我们不建议这样做,因为这两种语言和各自的生态系统最终都是非常不同的。
变量
在JavaScript中,有几种方法来定义变量:
const x = 1
let y = 5
console.log(x, y) // 1 5 are printed
y += 10
console.log(x, y) // 1 15 are printed
y = 'sometext'
console.log(x, y) // 1 sometext are printed
x = 4 // causes an errorconst实际上并没有定义一个变量,而是一个常量,其值不能再被改变。相对应的,let定义了一个普通变量。
在上面的例子中,我们还看到分配给变量的数据类型在执行过程中可以改变。在开始时y存储的是一个整数,在结束时是一个字符串。
在JavaScript中也可以使用关键字var来定义变量。在很长一段时间内,var是定义变量的唯一方法。 const和let是在2015年的ES6版本中引入的。在特定情况下,与大多数语言中的变量定义相比,var的运行方式有所不同——更多信息请参见Medium上的JavaScript Variables - Should You Use let, var or const?或JS Tips上的Keyword: var vs. let 。在本课程中,使用var是不明智的,你应该坚持使用const和let!
你可以在YouTube上找到更多关于这个主题的信息——例如 var, let and const - ES6 JavaScript Features
数组
一个数组和几个使用它的例子:
const t = [1, -1, 3]
t.push(5)
console.log(t.length) // 4 is printed
console.log(t[1]) // -1 is printed
t.forEach(value => {
console.log(value) // numbers 1, -1, 3, 5 are printed, each to own line
})需要注意的是在这个例子中,虽然用const声明的变量不可以再被重新赋值,但是对象引用的内容依然可以更改。这是因为const声明保证的是引用地址的不变性,而非引用数据的不变性。这就好比改变房子里的家具的时候,房子的地址还是一样的。
遍历数组项目的一种方法是使用forEach,如示例中所示。forEach接收一个用箭头语法定义的函数作为参数。
value => {
console.log(value)
}forEach为数组中的每一项调用函数,总是传递单个项作为参数。作为forEach参数的函数也可以接收其他参数。
在前面的例子中,使用方法push将一个新项添加到数组中。在使用React时,经常使用函数式编程的方法。函数式编程范式的一个特点是使用不可变的数据结构。在React代码中,最好使用concat方法,该方法会创建一个包含新项的新数组。这样可以保证原始数组保持不变。
const t = [1, -1, 3]
const t2 = t.concat(5)
console.log(t) // [1, -1, 3] is printed
console.log(t2) // [1, -1, 3, 5] is printed方法调用t.concat(5)并没有向旧数组添加一个新的项,而是返回一个新的数组,这个数组除了包含旧数组的项之外,还包含新的项。
数组定义了很多有用的方法。让我们看看一个使用map方法的简短例子。
const t = [1, 2, 3]
const m1 = t.map(value => value * 2)
console.log(m1) // [2, 4, 6] is printedmap基于旧数组创建了一个新数组,这个数组使用作为参数的函数来创建每一项。在这个例子中,是将原始值乘以2。
map也可以将数组转化为完全不同的东西:
const m2 = t.map(value => '<li>' + value + '</li>')
console.log(m2)
// [ '<li>1</li>', '<li>2</li>', '<li>3</li>' ] is printed这里通过map方法将一个充满整数值的数组转化为一个包含HTML字符串的数组。在本课程的第2章节中,我们看到map在React中的使用得相当频繁。
在解构赋值的帮助下,很容易将数组中的每个项目赋值给变量。
const t = [1, 2, 3, 4, 5]
const [first, second, ...rest] = t
console.log(first, second) // 1 2 is printed
console.log(rest) // [3, 4, 5] is printed上面,数组的第一个整数赋值给了变量first,数组的第二个整数赋值给了变量second。变量rest“收集”其余的整数到自己的数组中。
对象
在JavaScript中,有多种不同的方法来定义对象。一种非常常见的方法是使用对象字面量,也就是在大括号内列出其属性:
const object1 = {
name: 'Arto Hellas',
age: 35,
education: 'PhD',
}
const object2 = {
name: 'Full Stack web application development',
level: 'intermediate studies',
size: 5,
}
const object3 = {
name: {
first: 'Dan',
last: 'Abramov',
},
grades: [2, 3, 5, 3],
department: 'Stanford University',
}属性的值可以是任何类型的,比如整数、字符串、数组、对象……
一个对象的属性是通过“点”号或中括号来引用的:
console.log(object1.name) // Arto Hellas is printed
const fieldName = 'age'
console.log(object1[fieldName]) // 35 is printed你也可以通过使用点号或中括号来为一个对象即时添加属性:
object1.address = 'Helsinki'
object1['secret number'] = 12341后一种添加必须使用中括号,因为当使用点号时,由于有空格字符,secret number不是一个有效的属性名称。
自然地,JavaScript中的对象也可以有方法。然而在本课程中,我们不需要定义任何有自己方法的对象。这就是为什么在本课程中只简单地讨论它们。
对象也可以用所谓的构造函数来定义,这一机制让人想起许多其他编程语言,例如Java的类。尽管有这种相似性,JavaScript并没有与面向对象的编程语言一样的类。然而,从ES6版本开始,增加了class语法,这在某些情况下有助于构造面向对象的类。
函数
我们已经熟悉了定义箭头函数的方法。在不走弯路的情况下,定义一个箭头函数的完整过程如下:
const sum = (p1, p2) => {
console.log(p1)
console.log(p2)
return p1 + p2
}函数的调用和预期的一样:
const result = sum(1, 5)
console.log(result)如果只有一个参数,我们可以在定义中排除括号。
const square = p => {
console.log(p)
return p * p
}如果函数只包含一个表达式,那么大括号就不需要了。在这种情况下,函数只返回其唯一表达式的结果。现在,如果我们去掉控制台打印,我们可以进一步简化函数定义:
const square = p => p * p这种形式在操作数组时特别方便——例如使用map方法时:
const t = [1, 2, 3]
const tSquared = t.map(p => p * p)
// tSquared is now [1, 4, 9]箭头函数的功能是在2015年的ES6版本才加入到JavaScript中的。在这之前,定义函数的唯一方法是使用关键字function。
有两种方式来引用函数;一种是在函数声明中给出一个名称。
function product(a, b) {
return a * b
}
const result = product(2, 6)
// result is now 12另一种定义函数的方式是使用函数表达式。在这种情况下,不需要给函数一个名字,定义可以存在于代码的其他部分:
const average = function(a, b) {
return (a + b) / 2
}
const result = average(2, 5)
// result is now 3.5在本课程中,所有函数都使用箭头语法定义。
对象方法和“this”
由于本课程使用的是包含React Hooks的React版本,我们无需定义带方法的对象。这一章的内容与本课程无关,但在很多方面肯定是值得了解的。特别是在使用旧版本的React时,必须了解本章的主题。
箭头函数和使用function关键字定义的函数,在对关键字this,即对象本身的行为方式上有很大不同。
我们可以通过定义函数类型的属性来将方法赋值给对象:
const arto = {
name: 'Arto Hellas',
age: 35,
education: 'PhD',
greet: function() { console.log('hello, my name is ' + this.name) },}
arto.greet() // "hello, my name is Arto Hellas" gets printed即使在对象创建之后,也可以将方法赋值给对象:
const arto = {
name: 'Arto Hellas',
age: 35,
education: 'PhD',
greet: function() {
console.log('hello, my name is ' + this.name)
},
}
arto.growOlder = function() { this.age += 1}
console.log(arto.age) // 35 is printed
arto.growOlder()
console.log(arto.age) // 36 is printed让我们稍微修改一下对象:
const arto = {
name: 'Arto Hellas',
age: 35,
education: 'PhD',
greet: function() {
console.log('hello, my name is ' + this.name)
},
doAddition: function(a, b) { console.log(a + b) },}
arto.doAddition(1, 4) // 5 is printed
const referenceToAddition = arto.doAddition
referenceToAddition(10, 15) // 25 is printed现在这个对象有一个方法doAddition计算给它的参数的数字之和。该方法的调用方式和平常一样,使用对象arto.doAddition(1, 4),或者将方法引用存储到变量中,然后通过该变量调用该方法:referenceToAddition(10, 15)。
如果我们试图对方法greet做同样的事情,我们会遇到一个问题。
arto.greet() // "hello, my name is Arto Hellas" gets printed
const referenceToGreet = arto.greet
referenceToGreet() // prints "hello, my name is undefined"当通过引用调用方法时,该方法失去了对原始this的引用。与其他语言相反,在JavaScript中,this的值是根据方法的调用方式定义的。当通过引用调用方法时,this的值就变成了所谓的全局对象,最终的结果往往不是开发者最初的意图。
编写JavaScript代码时丢掉对this的跟踪带来了一些潜在的问题。当React或Node(或者更确切地说,网络浏览器的JavaScript引擎)需要调用开发者定义的对象中的某些方法时经常会出现这样的情况。然而,在本课程中,我们通过使用“无this”的JavaScript来避免这些问题。
当我们使用setTimeout函数设置超时来调用arto对象上的greet函数时,就会出现this“消失”的情况。
const arto = {
name: 'Arto Hellas',
greet: function() {
console.log('hello, my name is ' + this.name)
},
}
setTimeout(arto.greet, 1000)如前所述,JavaScript中this的值是根据方法被调用的方式来定义的。当setTimeout在调用方法时,是JavaScript引擎在实际调用方法,此时,this是指全局对象。
有几种机制可以保留原来的this。其中之一是使用方法bind。
setTimeout(arto.greet.bind(arto), 1000)调用arto.greet.bind(arto)创建一个新的函数,其中this被绑定为指向Arto,与调用该方法的地点和方式无关。
使用箭头函数可以解决一些与this有关的问题。然而,它们不应该被用作对象的方法,因为那样的话this就完全不起作用了。我们稍后会回到this与箭头函数相关的行为上。
如果你想更好地了解this在JavaScript中是如何运行的,互联网上有很多关于这个主题的资料,例如,强烈推荐egghead.io的截屏系列Understand JavaScript's this Keyword in Depth!
类
如前所述,在JavaScript中没有像面向对象编程语言中的类机制。然而,有一些功能可以“模拟”面向对象的类。
让我们快速浏览一下ES6引入JavaScript的类语法,它大大简化了JavaScript中类(或类似类的东西)的定义。
在下面的例子中,我们定义了一个名为Person的 “类”和两个Person对象。
class Person {
constructor(name, age) {
this.name = name
this.age = age
}
greet() {
console.log('hello, my name is ' + this.name)
}
}
const adam = new Person('Adam Ondra', 35)
adam.greet()
const janja = new Person('Janja Garnbret', 22)
janja.greet()在语法上,JavaScrip的类及其创建的对象很容易让人联想到Java的类和对象。JavaScript的类及其创建的对象的行为也与Java的类和对象对象相当相似。但JavaScript类创建的对象的内核仍然是基于原型继承的普通的JavaScript对象。任何类的对象实际上都是Object,因为JavaScript定义的类型只有Boolean、Null、Undefined、Number、String、Symbol、BigInt和Object。
类语法的引入是有争议的。请查看Not Awesome: ES6 Classes或Medium上的Is "Class" In ES6 The New "Bad" Part?了解更多细节。
ES6类的语法在“老”React和Node.js中用得很多,因此即使在这个课程中,对它的理解也是有益的。然而,由于我们在整个课程中使用React的新Hook功能,我们对JavaScript的类语法没有具体的使用。
JavaScript资料
互联网上的JavaScript指南良莠不齐。本页面中大多数与JavaScript特性有关的链接都参考了Mozilla的JavaScript指南。
强烈建议立即阅读Mozilla网站上的JavaScript语言概览。
如果你想深入了解JavaScript,网上有个很棒的免费丛书,叫做You-Dont-Know-JS。
另一个学习JavaScript的好资源是javascript.info。
Eloquent JavaScript既免费又引人入胜,它能够帮助你快速从基础入门到深入学习。这本书结合了理论、项目和练习,既涵盖了通用编程理论,也介绍了JavaScript语言本身。
Namaste 🙏 JavaScript是另一个非常棒且强烈推荐的免费JavaScript教程,可以帮助你理解JS的底层原理。Namaste JavaScript是一个纯深入的JavaScript课程,在YouTube上可以免费观看。它详细讲解了JavaScript的核心概念以及JS在JavaScript引擎内部的运行机制。
egghead.io有大量关于JavaScript、React和其他有趣话题的高质量截屏。不幸的是,有些资料需要付费。