跳到内容

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 error

const实际上并没有定义一个变量,而是一个常量,其值不能再被改变。相对应的,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 printed

map基于旧数组创建了一个新数组,这个数组使用作为参数的函数来创建每一项。在这个例子中,是将原始值乘以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 ClassesMedium上的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和其他有趣话题的高质量截屏。不幸的是,有些资料需要付费。