b

JavaScript

在本课程中,除了网页开发,我们还有一个目标和需求,就是学习足量的 JavaScript 知识。

JavaScript 在过去的几年里发展非常迅速,在本课程中,我们将使用新版本的特性。 JavaScript 标准的正式名称是ECMAScript。 目前(2021年1月,译者注),最新的版本是2021年6月发布的,名为ECMAScript®2021 ,即ES12。

浏览器还不能支持所有 JavaScript 的最新特性。 基于这个事实,许多在浏览器中运行的代码需要从一个新版本的 JavaScript 转译到了一个更旧、更兼容的版本。

如今,最流行的转译方法是使用 Babel。 在使用 create-React-app 创建的 React 应用中转译是自动配置好的。 我们将在本课程的第7章节中仔细研究转译的配置。

Node.js是一个基于谷歌的 chrome V8 引擎的 JavaScript 运行时环境,可以在任何地方工作,从服务端到移动端。 让我们练习使用 Node 编写一些 JavaScript。 您机器上安装的 Node.js 版本至少是 v14.8.0。 最新版本的 Node 能够理解 JavaScript 最新版本的特性,因此代码不需要被转译。

代码文件以 .js结尾,通过 node 文件名.js 命令以运行文件。

还可以将 JavaScript 代码编写到 Node.js 控制台(通过在命令行中键入 node 打开),或者浏览器的开发工具控制台中。 最新版本的 Chrome 能很好地处理 JavaScript 的新特性 ,而且不需要转译代码。作为替代品,你也可以选择 JS Bin这样的工具。

JavaScript 都有点像 Java。 但是当涉及到语言的核心机制时,它们就大不相同了。 如果你是 Java 背景,JavaScript 的写法可能看起来有点古怪,尤其是如果你不花精力去查阅它的特性的话。

在某些圈儿里,也很流行在 JavaScript 中“模拟” Java 的特性和设计模式。但我们不建议这样做。

Variables

【变量】

在 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 存储了一个整数,但最后存储一个字符串。

也可以使用关键字var在 JavaScript 中定义变量。 在很长一段时间里,var 是定义变量的唯一方法。 const 和 let 是最近才在 ES6版本中添加的。 在一些特定情况,var 的工作方式与大多数语言中的变量定义相比是十分不同的, 可以阅读JavaScript Variables - Should You Use let, var or const? on Medium 或者Keyword: var vs. let on JS Tips。 在本课程中明确不建议使用var,你应该坚持使用 const 和 let!

你可以在 YouTube中找到更多关于这个 var, let and const - ES6 JavaScript Features议题的讨论

Arrays

【数组】

以下是数组和它的几个使用示例:

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 定义,也可以修改该数组中的内容。 因为数组是一个对象,而数组变量总是指向这同一个对象。 当添加新的元素时,数组的内容也将发生变化。

遍历元素的一种方法是使用 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

由于这种解构赋值方式,变量 firstsecond 将接收数组的前两个整数作为它们的值。 剩余的整数被“收集”到另一个数组中,然后分配给变量 rest。

Objects

【对象】

在 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版本开始,增加了类语法,这在某些情况下有助于构造面向对象的类。

Functions

【函数】

我们已经了解了箭头函数的定义。 定义箭头函数的完整过程如下:

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]

这个箭头函数是几年前随 ES6 一起添加到 JavaScript 中。 在此之前,定义函数的唯一方法是使用关键字 function

有两种方法可定义函数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

在本课程中,我们将使用箭头语法定义所有函数。

Object methods and "this"

【对象方法以及“ this”关键字】

由于在本课程中我们使用的React版本里包含 React hook ,所以我们不需要定义带有函数的对象。 因此本章的内容与本课程无关 ,但在许多方面确实值得了解。 特别是在使用旧版本的 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"

当通过引用调用referenceToGreet() 方法时,该方法已经不认识原始的this是什么了。 与其他语言相反,在 JavaScript 中,this的值是根据 方法如何调用 来定义的。 当通过引用调用该方法时, this 的值就变成了所谓的全局对象 ,而最终结果往往不是软件开发人员设想的那样。

失去对this 关键字的追踪,在编写 JavaScript 代码时会带来一些潜在的问题。 通常情况下,React 或 Node (或者更确切地说是 web 浏览器的 JavaScript 引擎) 需要调用开发人员定义的对象中的某个方法。 然而,在本课程中,我们会使用“ 去this” (避免使用this关键字)的JavaScript 来避免这些问题。

例如,当我们使用setTimeout方法,让arto对象1秒钟后调用greet。

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与箭头函数的关系。

如果你想更好地理解 JavaScript 的工作原理,互联网上充满了关于这个议题的材料,例如 egghead.io的一系列Understand JavaScript's this Keyword in Depth短视频,强烈推荐!

Classes

【类】

正如前面提到的,JavaScript 中并没有像面向对象程序语言中的类机制。 然而,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()

在语法方面,类以及由它们创建的对象非常类似于 Java 的类和对象。 它们的行为也非常类似于 Java 对象。 但在本质上,它们仍然是基于 JavaScript 的原型继承的对象。 这两个对象的类型实际上都是 Object,因为 JavaScript 实质上只定义了Boolean,Null,Undefined,Number,String,Symbol,BigInt,以及 Object几种类型。

类语法的引入是一个有争议的新特性,例如Not Awesome: ES6 Classes 或者Is “Class” In ES6 The New “Bad” Part? on Medium这两篇文章所讨论的。

ES6的类语法在“老的” React 和 Node.js 中被广泛使用,因此即使在本课程中,对它有所了解也是有益的。 但是因为我们在整个课程中都使用了 React 的新的hooks特性,所以我们没有具体使用 JavaScript 的类语法。

JavaScript materials

【JavaScript 教材】

互联网上的 JavaScript 指南既有好的,也有不好的。 这个页面上大多数与 JavaScript 特性相关的链接都参考了 Mozilla 的 JavaScript 指南Mozilla's JavaScript Guide

强烈建议你立即在 Mozillas 网站上阅读重新认识JavaScript(JS 教程)

如果你想深入了解 JavaScript,互联网上有一个很棒的免费书系列叫做You-Dont-Know-JS

egghead.io 上有大量关于 JavaScript、 React 及其他有趣议题的高质量短视频。不幸的是,有些材料是付费后才能看的。