入门JavaScript(一):印象与语法
学习JavaScript之前,我们得知道这是一门怎样奇葩的语言:
- 这门语言有长达近30年的历史
- 设计这门语言只用了十天
- 曾长期没有官方的包管理机制
- 直到现在,面向对象系统也非常粗糙
- 违反直觉的隐式类型转换能把新手干懵
- 最初设计的目的只是让网页更生动,但现在啥都能用JS写
- Python,C等语言一般只有一个主流的解释器/编译器,但JS有浏览器(Chrome/Edge)内置的V8解释器、服务端的虚拟机nodejs(以及更多,Deno、Bun等),两者的应用场景、支持的功能都有本质不同
理所当然地,JavaScript存在各种演化遗留问题,甚至显得滑稽臃肿,与“优雅”毫不沾边。但无可质疑的是:
- 这是唯一被所有浏览器支持的编程语言
- 极其关注事件的异步处理
- 拥有最繁荣的开发社区
- 有许多语言在编写之后,需要转换为JS运行。例如TypeScript,Dart,Kotlin
这也为我们的学习带来便利。只要你打开浏览器再按下 F12
,就会出现一个控制台(console),在里面就可以写JS了!比如,打印一个 Hello World!
那么,让我们深吸一口气,推开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
null
和 undefined
代表空值,只在少数时候区分两者
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
内的元素是否相同(这种操作被称为“深度比较”),需要通过第三方包如 Lodash
的 isEqual
函数实现。
在浏览器的控制台中,需要用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
函数的作用是弹出提醒
然后把文件后缀名改为 .html
,双击打开,就会看到JS代码的效果。如果你使用 console.log()
输出到控制台,记得按 F12
查看
当然,你也可以导入另一个JS脚本。但注意指定 src=
引入脚本后,当前 <script>
标签内部的代码就不会生效了
<script src="/path/to/t.js"></script>
结语
本文我们学习了JavaScript的数据类型、函数、流程控制,以及如何在浏览器控制台、HTML文档中使用。有了这些知识,你可以写出一些打印、算术的小玩意了。
如果需要更多参考,这份JavaScript现代教程和MDN JavaScript参考非常适合。
最后来张梗图,玩得开心!