学习JavaScript之前,我们得知道这是一门怎样奇葩的语言:

  • 这门语言有长达近30年的历史
  • 设计这门语言只用了十天
  • 曾长期没有官方的包管理机制
  • 直到现在,面向对象系统也非常粗糙
  • 违反直觉的隐式类型转换能把新手干懵
  • 最初设计的目的只是让网页更生动,但现在啥都能用JS写
  • Python,C等语言一般只有一个主流的解释器/编译器,但JS有浏览器(Chrome/Edge)内置的V8解释器、服务端的虚拟机nodejs(以及更多,Deno、Bun等),两者的应用场景、支持的功能都有本质不同

理所当然地,JavaScript存在各种演化遗留问题,甚至显得滑稽臃肿,与“优雅”毫不沾边。但无可质疑的是:

  • 这是唯一被所有浏览器支持的编程语言
  • 极其关注事件的异步处理
  • 拥有最繁荣的开发社区
  • 有许多语言在编写之后,需要转换为JS运行。例如TypeScript,Dart,Kotlin

这也为我们的学习带来便利。只要你打开浏览器再按下 F12,就会出现一个控制台(console),在里面就可以写JS了!比如,打印一个 Hello World!

image.png

那么,让我们深吸一口气,推开JavaScript的大门吧!


// print
console.log('Hello, World!');
/* multi-line 
comments */
const name = 'william';
let age = 25;
console.log(name + ' ,' + age + ' years old.');
console.log(`${name}, ${age} years old.`);

如上方代码所示,JS单行注释使用 //,多行注释使用 /* */,每句代码末尾需要 ;。其实 ;并非严格要求,但为了避免错误,最好都加上。

const关键字声明常量,let关键字声明变量。一个好习惯是除非有必要,默认使用常量。你可能会看到有人用 var声明变量,但这种语法已经过时了。

单引号与双引号没有区别。反引号(键盘Tab上方的那个键)可以用于插值表达式,在内部以 ${x}的形式引用变量。

数据类型

JS的数据类型有 String, Number, Boolean, Array, Object等,后两者分别是列表和键值对。可以通过类似 x instanceof Array的方式判定

String object

对于字符串,常用的方法有

// 取字符
s[0]
// 按起止索引取字符串
s.substring(0, 5)
// 检测子字符串
s.includes('wo')
// 返回字符串的位置,找不到返回-1
s.indexOf('wo') 
// 拼接
s1 + s2 
// 字符串插值
`hello, ${name}` 
// 分割
s.split('\n')
// 去除前后空格
s.trim()
// 转为string原始类型
x.toString()
// 增加对null, undefined的支持
String(x)

使用 //包裹正则表达式

// 正则匹配,返回Array object的匹配列表
s.match(/pattern/)
// 正则匹配,返回boolean原始类型
/pattern/.test(s)

Number object

对于数字,常用的方法有

// 转为number原始类型,null返回0,undefined返回NaN
Number(x)
// 保留两位小数,返回string
x.toFixed(2)
// 保留两位有效数字,返回string
x.toPrecision(2)

NaN属于 Number类型,代表Not a Number,它与任何值计算都会得到 NaN

nullundefined代表空值,只在少数时候区分两者

Boolean object

Boolean(x)
// true
1 == true
0 == false

&& || !进行逻辑运算。JS近期引入了控制合并运算符,如 a ?? b,意为当a不为空时返回a,为空时返回b

Array object

数组,可嵌套,可包含任意类型,可具名。常用的方法有

let l = [1,'B', [2, 'C']]
l[0]
// 快速创建数组
l = Array.from({ length: 5 }, (_, index) => index * 2)
// 返回元素个数
l.length
// 返回元素索引
l.indexOf('B')
// 按起止索引切片
l.slice(0,1)
// 向末尾添加元素
l.push()
// 向头部添加元素
l.unshift()
// 排序,无返回值
l.sort()
// 返回新array。会展平追加的array
l.concat(4,[5,6])
// 合并array为字符串
l.join(', ')
// 判断包含,但仅对一维数组有效
l.includes('hello')
// 判断包含,对二维及以上的数组
l.some(item => item[0] === "A" && item[1] === "B")
// 筛选
l.fliter(item => item > 2)
// 根据子元素的count属性排序
l.sort((x, y) => {return x.count - y.count})

可以使用解构赋值,快速为新变量赋值

let [el1, , el3] = l
let [el1, ...el23] = l

Object object

无序键值对,键需要是string,部分情况可以省略引号;值可以是任意类型

从面向对象的角度看,每个键都是对象的属性,当值是函数时,也就给对象绑定了方法

// 无论key是否加引号,都被转为字符串
// 直接书写变量名的项,会自动解析为'变量名':值
const city = 'Beijing'
let d = {name:'C', age:20, 'mid-school': 'No.1', city}
// .和[]是等效的
d.name
d['mid-school']
d?.name
// 删除元素
delete d.age
// 判断是否包含某属性
'age' in d
// 获取键,返回数组
keys(d)
// 获取值,返回数组
values(d)
// 获取键值对,返回数组
Object.entries(d)
// 合并
const d3 = { ...d1, ...d2}
// 判断属性是否存在
"age" in d
// 合并其他Object的属性
Object.assign(res, d1, d2, d3)

可使用解构赋值,顺序不敏感,甚至可以指定默认值

// 使用key作为新变量名
let {name, age} = d
// 指定新的变量名
let {name: var1, age: var2} = d
// 指定默认值
let {name: var1, age: var2, birthday='0102'} = d

可以像这样将变量的值指定为key,称为计算属性

const keyA = "age";
let d = {name:'C', [keyA]:20, 'mid-school': 'No.1'}

创建属性时,可以直接引用变量,达到简写的效果

const name = "william"
let d1 = {name: name, age: 20}
let d2 = {name, age: 20}

由于Object存储的是引用而不是值,即使用 const声明变量,仍然可以修改属性。除非换掉整个Object

const d = {name:"william"}
// 正常
d.age = 20
// 报错
d = {name: "sigma"}

使用可选链 ?.避免报错

const d = {A:1}
d.B // 不报错
d.B.C // 报错
d.B?.C // 不报错
d.B?.C?.D // 不报错

原始类型与引用类型

我们按照其他编程语言的思路,概览了JS的基本数据类型。但JS有个特点,是可感知地区分了原始类型和引用类型,以上所述是根据引用类型划分的。

形如 "123", 123, true的值属于原始类型,而 new String("123")的值属于引用类型。原始类型只有 undefined, boolean, string, number, object, function, symbol七类,使用 typeof查看

引用类型都是 object原始类型,包括以上所述 String, Array, Object等。其中 String, Number, Boolean与原始类型 string, number, boolean对应

原始类型并不支持如 s.substring的方法,实际上方法本就是 object的特权。当我们使用 let x = 'hello'时,创建了一个 string原始类型。在调用如 s.substring的方法时,后台会先创建一个 String引用类型实例(有时也叫包装类型),调用相应方法,再销毁实例还原到原始类型

所以我们才会看到这样的结果

typeof("123") // string
typeof(String("123")) // string
typeof(new String("123")) // object
"123" instanceof Object // false
"123" instanceof String // false
new String("123") instanceof Object // true
new String("123") instanceof String // true

形如 [1,2,3], {a:1}的值本就是引用类型,typeof的结果都是 object

typeof([1,2,3]) // object
typeof({a:1}) // object
[1,2,3] instanceof Object // true
[1,2,3] instanceof Array // true
({a:1}) instanceof Object // true

判断相等

原始类型与引用类型的区分,在判断相等时可能导致预期外的结果。我们仔细梳理一下

对于原始类型 string, number, boolean之间的比较,==会尝试转换为相同的原始类型,然后再比。===则不会转换

// true
1 == '1'
1 == true
0 == false
'1' == true
String(1) === '1'
// false
1 === '1'

对于原始类型 object之间的比较, ==仅在两者在同一个内存地址时,才返回 true。注意通过 new关键字声明的 Number, String等也是 object

// false
({a:1} == {a:1})
new Object({a:1}) == new Object({a:1})
[1, 2] == [1, 2]
new Number(2) == new Number(2)

对于 object和其他原始类型的比较,JS会尝试将 object转换为其他原始类型,然后比较

// true
new Number(2) == 2

总结就是,在大多数场合,建议使用 ===比较。JS不能判断两个 Array, Object内的元素是否相同(这种操作被称为“深度比较”),需要通过第三方包如 LodashisEqual函数实现。

在浏览器的控制台中,需要用JS创建一个 <script>标签,通过CDN链接载入 Lodash包。如果你是在 Github等网站调出的控制台,由于网站的安全策略,该操作会被阻止。可尝试换一个网站开启控制台,比如百度

let script = document.createElement('script');
script.src = 'https://cdn.jsdelivr.net/npm/lodash@4.17.21/lodash.min.js';
document.head.appendChild(script);

载入 Lodash包后,包内的方法通过 _.xxx调用。你可能疑惑直接占用 _是否会造成命名冲突,但JS里大家就这么约定俗成地用了下来

_.isEqual({a: 1}, {a: 1}); 

函数

创建带返回值的函数,有三种形式

function foo(a, b) { return a + b }
const foo = (a, b) => a + b
const foo = (a, b) => { return a + b }

前者是声明式函数,后两者称为箭头函数。return需要显示指定,除非采用第二种方法,即函数体简短、不使用 {}的箭头函数

当箭头函数有0个或多个参数时,()不可省略。仅有一个参数时,()可以省略

JS早期版本不支持设置参数默认值,所以老代码中你可能会看到这样设置默认值

b = b || "default B value"

调用方法时,JS只支持位置参数,不支持关键字参数。所以你会看到许多函数借助解构赋值,实现类似关键字参数的效果

function foo(a=1, b) { return a + b }
foo(b=2) // 报错

function foo({a=1, b}) { return a+b }
foo({b:2})

如果函数绑定到Object中,就被称为方法。声明式函数内部,可以调用 this代指当前对象

let d = {
	name: "william",
	sayHi: function sayHi() {
		return "Hi, " + this.name
	}
}

d.sayHi()

// 简写
let d = {
	name: "william",
	sayHi() {
		return "Hi, " + this.name
	}
}

而箭头函数的 this,并不代指当前对象,反而代指更外层的上下文

let d = {
	name: "william",
	sayHi: () => { return "Hi, " + this.name }
}

d.sayHi() // Hi, 

let d = {
	name: "william",
	sayHi() {
		const f = () => { return "Hi, " + this.name }
		return f()
	}
}

d.sayHi() // Hi, william

流程控制

条件语句

if (cond) {cmd1} else if (cond2) {cmd2} else {cmd3}
// 简写
cond ? cmd1 : cmd2

循环语句

// while
while (cond) {cmd}
// do while
do {cmd} while (cond)
// for
for (begin; cond; step) {cmd}

循环体中,使用 break关键字跳出循环,continue关键字立即开始下一个循环。如果需要跳出多层循环,可使用break标签

对于object,可使用 for...in语句遍历所有键

for (const key in d) {
	console.log(key)
	console.log(d[key])
}

对于Array,可使用 for...of语句遍历所有值

for (const value of l) {cmd}

由于数组也是object,所以也可用 for...in遍历索引,但通常不建议这么做

由于 Object.entries()返回键值对的数组,所以可以如此遍历Object键值对

for (const [key, value] of Object.entries(d)) {
    console.log(key, value);
}

此外,数组可以使用 .forEach.map方法,将一个函数应用到每个元素上。前者无返回值,后者返回新的 Array

let l = []
d = [{foo:1}, {foo:2}]
d.forEach( item => l.push(item.foo) )

let l = [1, 2, 3]
l = l.map(value => value*2)
// 每个遍历项都可以拆分为value和index
l = l.map((value, index) => value*2 + index)

错误处理

// 错误捕获结构
try {cmd1} catch (err) {cmd2} finally {cmd3}
// 主动抛出异常
throw new Error("Something is wrong")

嵌入HTML

在实际使用中,JS都要插入在HTML文档。可以新建一个文本文件,像这样在 <script>标签中插入JS代码。alert函数的作用是弹出提醒

image.png

然后把文件后缀名改为 .html,双击打开,就会看到JS代码的效果。如果你使用 console.log()输出到控制台,记得按 F12查看

image.png

当然,你也可以导入另一个JS脚本。但注意指定 src=引入脚本后,当前 <script>标签内部的代码就不会生效了

<script src="/path/to/t.js"></script>

结语

本文我们学习了JavaScript的数据类型、函数、流程控制,以及如何在浏览器控制台、HTML文档中使用。有了这些知识,你可以写出一些打印、算术的小玩意了。

如果需要更多参考,这份JavaScript现代教程MDN JavaScript参考非常适合。

最后来张梗图,玩得开心!

image.png