JavaScript 入门指南
JavaScript的历史
JavaScript问世于1995年,最初是用于控制浏览器页面行为的脚本语言,例如验证某个字段是否已经填写,或者测试输入值的合法性。后来,欧洲计算机制造商协会(Ecma)下属的TC39委员会发布了ECMA-262,作为ECMAScript的语言标准。1998年,ECMAScript被纳入国际标准。此后,各大浏览器厂商以ECMAScript标准为依据,分别实现了自己的JavaScript语言。时至今日,对ECMAScript支持最好的浏览器是Google Chrome和Microsoft Edge,这两个浏览器均使用了Chromium开源内核。Chromium基于Google开源的V8引擎。
除了作为浏览器的脚本语言,在2009年,Node.js问世。Node.js是一个基于Chrome V8引擎的JavaScript运行时。Node.js在语法上采用了JavaScript语言,同时带来了用于操作服务端的模块,例如:文件系统模块fs、操作系统模块os。Node.js将JavaScript的宿主环境从浏览器端带到了服务器端。
经过了27年的发展,如今的JavaScript已经成长为了最流行的语言之一,以及开发者们最喜爱的编程语言之一。
JavaScript的组成
JavaScript由ECMAScript、BOM、DOM三部分组成:
-
EcmaScript:即ECMA-262定义的国际标准,定义了语法规则、数据类型、关键字、模块、全局对象等核心语言特性。
-
DOM(Document Object Model):文档对象模型,规定了JavaScript如何操作HTML元素。
-
BOM(Browser Object Model):浏览器对象模型,用于与浏览器窗口交互,例如页面导航、浏览历史、离线存储等。
ECMAScript 6 发布以来的新增特性
ECMAScript 标准自2015年以来每年6月会发布一个新版本。ECMAScript 的某个特性从想法到最终的标准一般会经历stage0——stage4 五个阶段,分别是:strawman(最初想法)、proposal(提案)、draft(草案)、candidate(候选)、finished(完成)。一般而言,一个提案一旦进入草案阶段就很有可能会纳入最终的标准。
在以前,从标准的发布到浏览器厂商的支持又会经历很长的时间,这时可以使用polyfill框架用于实现浏览器暂不支持的特性。不过现在这种情况已经得到了好转,甚至对于很多特性,往往是浏览器厂商率先推出,而后TC39委员会将其作为标准纳入。
下面列出了自ECMAScript 2015 发布以来的新增特性:
-
ES6:发行于2015年6月,新增了许多影响深远的特性:箭头函数、模块、迭代器、生成器、期约、反射、代理。ES6的发布对于JavaScript来说是一个里程碑事件,这是经典JavaScript和现代JavaScript的分水岭,奠定了JavaScript繁荣的基础。之后的ES版本也可以统称为“ES6”。
-
ES7:发布于2016年6月,新增了指数操作符。
-
ES8:发布于2017年6月,async/await、Object.values()/keys()/entries()、对象字面量拖尾逗号。async和await又是现代JavaScript发展史上另外一个极其重要的特性。
-
ES9:发布于2018年6月,新增了Promise finally()、异步迭代、剩余和扩展属性。
-
ES10:发布于2019年6月,新增了数组打平、字符串定长填充等特性。
-
ES11:发布于2020年6月,新增了可选链、空位合并等特性。
-
ES12:发布于2021年6月,新增了Promise any()、String.prototype.replaceAll()等特性。
-
ES13:发布于2022年6月,新增了顶层await、Array.prototype.at()、class私有方法 / 静态方法等特性。
原始数据类型
布尔类型
布尔类型只有两个字面量:true和false。
下面这些值视作true:
- 非空字符串,注意," "有一个空格,也算非空。
- 任意对象,包括空对象
- 正负无穷大
- 非零数值
下面这些值视作false:
- 空字符串""
- NaN
- 数字0
- null
- undefined
可以使用!!()
和Boolean()
将其它形式的值转为布尔类型,以便于条件判断,前者是后者的简写形式。
来看下面的示例:
// 下面这些值都视作true
Boolean(hello') // => true
Boolean(' ') // 非空字符串,空格也算非空
Boolean(5/0) // 正负无穷大
Boolean({}) // 任意对象,包括空对象
// 下面这些值都视作false
Boolean('') // =>false 空字符串
Boolean(0)
Boolean(NaN)
Boolean(null)
Boolean(undefined)
在if语句中,不必显式地使用!!()
,会自动隐式调用:
const a=2
if (a){console.log(true)} //=> true
let b
if (b){console.log(true)}
else {console.log(false)} //=> false
数值类型
使用如下方式新建数值类型的变量:
let num = 1 // 整数
let floatNum = 0.1 // 浮点数
let num=1.0 // 虽然跟了小数点,但依然会被处理成整数
NaN是一种特殊的数值,表示运算错误,但不会报错。Infinity表示无穷大,-Infinity表示无穷小,例如:
0/0 //=> NaN
5/0 // => Infinity,很多语言会报错,但是这里为无穷大
5/-0 // => -Infinity
有三种函数用于将其它形式的值转为数值,Number()是通用的,可将类似数值的字符串或单数值元素的数组转换为数组。parseInt()和parseFloat()用于将开头是数字而存在非数字的字符串提取成数值,例如:
Number(true) // 遇布尔值转为1或0
Number(null) // 0
Number(undefined) // NaN
Number('123') // =>123
Number('123 456') // => NaN
Number('123hello') // => NaN
Number('') //=>0
parseInt('123hello') //=> 123
parseFloat('1.23hello') //=> 1.23
由于乘法运算和减法运算在碰到数值时会隐式调用Number(),因此可以使用此方法快速调用Number(),例如:
true * 1 //=>1,等同于Number(true)
'123' * 2 // => 246,等同于Number('123')
字符串类型
字符串模板字面量
模板字面量取代了早期和其它语言的%d、%f等写法,使得变量化的字符串更容易书写,也更易阅读。模板字符串使用反单引号(``)包容,它有最主要的两个特点:保留了换行符等不可见字符(以往只能用\n);提供了变量解析和运算。
let str = `第一行 (这里按回车)
第二行`
str // =>'第一行\n第二行'
let a=1
let b=2
`a的值是${a}` // =>'a的值是1'
`a+b的结果是${a+b}` // => 'a+b的结果是3'
虽然string类型是原始值,但是表现出像对象一样使用属性和方法。
let str='hello'
str.length // =>5, 字符串的长度
[...str] //=>[ 'h', 'e', 'l', 'l', 'o' ] ,将字符串快速打平为数组
slice()、substring()、substr()
要提取子字符串,有三种方法。slice()和substring()需要传入提取开始的位置和结束位置,而substr()需要传入开始位置和提取的字数量。
let str='hello,world'
str.slice(4,7) //=>'o,w' ,从索引4位置开始提取,到索引7位置之前结束(左闭右开原则)
str.slice(4) //=> 'o,world',从索引4位置开始提取,一直到结束
str.substr(4,3) // =>'o,w',从索引4位置开始提取,提取3个字符
indexOf()
有两种方法返回字符或子字符串在字符串中的索引位置:
let str='hello,world'
str.indexOf('w') //=>6,在索引6位置处找打了就停止查找
str.lastIndexOf('r') //=>8,反过来找,在索引8位置处找到了就停止查找
str.indexOf('wo') //=>6,查找子字符串
includes()、startWith()、endsWith()
有三种方法进行字符串的包含判断:
let str='hello,world'
str.includes('hello') //=>true,是否包含
str.startsWith('hello') //=> true,是否以子字符串开始
str.endsWith('world') //=>true,是否以子字符串结束
trim()、trimLeft()、trimRight()
有三种方法去除字符串的首尾空格:
let str=' hello,world '
str.trim() //=>'hello,world',去除首尾空格
str.trimLeft() //=>' hello,world',只去除左边的空格
str.trimRight() //=>' hello,world',只去除右边的空格
repeat()
使用repeat()方法进行字符串的重复操作
let str='hello'
str.repeat(3) //=>'hellohellohello'
padStart()、padEnd()
有时候需要保证字符串的长度是固定的,就需要在左右使用字符进行填充。
let str='hello'
str.padStart(10) //=> ' hello',在左侧填充默认的5个空格
str.padEnd(10) //=>'hello ',在右侧填充5个空格
str.padStart(3) //=>'hello',长度足够,原样返回
str.padStart(10,',') // =>',,,,,hello',在左侧使用逗号填充
字符串的大小写转换
可以使用如下两种方法进行字符串的大小写转换
let str='Hello'
str.toLowercase() // =>'hello'
str.toUpperCase() // =>'HELLO'
函数
箭头函数
箭头函数省去了function关键字,改而使用胖箭头来隔开参数列表和函数体:
let fun = (arg1,arg2,...) =>{
// statements
}
箭头函数通常被当作参数传递给其它函数使用,例如:
const arr=[1,2,3]
arr.forEach(x=>x*2)
console.log(arr) //=>[2,4,6]
使用箭头函数有几个注意事项。
第一,当参数只有一个参数时,可不加圆括号。没有参数或者多于1个参数,都需要加圆括号,例如:
const fun1 = x=>x+1 // 只有一个参数
const fun2 = ()=>1 // 没有参数
const fun3 = (x,y)=>x+y // 多于一个参数
第二,当箭头函数的函数体只有一行,并且这一行是赋值、打印、返回值的时候,不能加花括号,也不能写return,例如:
// 这两种写法都是错的:
const fun1= x => return x+1
const fun2= x => {return x+1}
// 这三种写法是对的:
const fun3= x =>x+1
const fun4= x =>console.log(x)
const fun5= x =>x.a=1
第三,当箭头函数的函数体只有一行,并且这一行返回一个对象时,需要在花括号两边加上圆括号,例如:
const fun = () => ({a:1,b:2})
console.log(fun()) // {a:1,b:2}
暂时性死区
参数是按顺序被赋值的,因此,前面的参数不可以引用后面的参数的默认值,也不能引用函数体中的成员值,这就是“暂时性死区”规则,例如:
function example(a=b, b=1,c=data){
const data=1
}
这段代码有两处错误:
- 参数a不能引用后面的参数b的值
- 参数c不能引用后面的函数体成员data的值
而这个例子是正确的:
function example(a=1 , b=a){
const data=b
console.log(data)
}
example() //=>1
其实,简单来讲,所谓暂时性死区,不过也遵循了局部作用域的声明规则。使用let和const声明的时候,声明和引用是按顺序来的,即只能先声明后引用,后面的引用前面的,反过来不可以,不存在声明提升。
默认参数值
在定义函数时,可以为参数赋予一个默认值。如果调用该函数时没有传递实参,那么就会使用默认值传递,这比以往的默认undefined值更方便了一步。
function sum(a=0,b=0){
return a+b
}
console.log(sum()) // => 0
console.log(sum(1)) //=> 1
console.log(sum(1,2)) //=>3
参数收集和参数扩展
定义函数时,如果不确定参数的个数,可以进行参数收集。参数收集的意思是只定义一个参数列表,未来传递实参时,无论参数有多少个,都会作为一个数组传递进来。这样我们就解决了参数个数不确定的问题,例如定义一个求和函数:
function sum(...values){
const result= values.reduce((prev,cur)=>prev+cur)
console.log(result)
}
这里将参数打包成一个数组,函数只针对数组进行处理,规避了参数个数不确定的问题。调用函数时,使用sum(1,2)或者sum(1,2,3,4)都是能进行求和的,因为总是会打包成一个数组处理:
[1,2].reduce((prev,cur)=>prev+cur)
[1,2,3,4].reduce((prev,cur)=>prev+cur)
现在,假设有一个现成的数组:
const arr=[1,2,3,4]
我们想调用上面定义的sum()函数对其元素进行求和,我们就需要先将这些元素一一取出,再依次传参,就像这样:
sum(arr[0],arr[1],arr[2],arr[3])
这无疑是麻烦的,使用扩展操作符,可以自动将数组解包:
sum(...arr)
这一行将会被解析为:
sum(1,2,3,4)
这就是参数扩展。
另外,请注意,大家可能会跟上面的参数收集搞混,认为直接传数组是可以的,实际上,如果直接传递数组sum(arr),那么函数体中就会是这样的操作:
[[1,2,3,4]].reduce((prev,cur)=>prev+cur)
这是无法求出结果的。
可以看到,参数收集和参数扩展分别用于函数定义和函数调用。一个将形参列表打包,一个用于实参的快速解包。
函数内部的arguments对象
对于使用了function关键字的函数声明或函数表达式,函数内部有一个arguments对象,这是一个类数组对象,可以通过Array.from(arguments)将其转化为数组。arguments.length表示实参的个数。arguments[n]表示第n个参数。
注意,箭头函数没有arguments对象。
有了arguments对象,即便是不写形参,也可以定义函数,例如:
function sum(){
const result=Array.from(arguments).reduce((prev,cur)=>prev+cur)
return result
}
console.log(sum(1,2))
console.log(sum(1,2,3))
函数内部的this对象
this,顾名思义,就是“这个”。
this被用在对象的方法中,表示“这个对象”。
函数可以被用作对象的方法。同一个函数,被不同对象调用时,上下文是不一样的,this指代调用的上下文对象。
function sayName(){
console.log(`I am ${this.name}`)
}
const zhangsan = {
name: 'Zhang San',
sayName: sayName
}
const lisi = {
name : 'Li Si',
sayName : sayName
}
zhangsan.sayName() // I am Zhang San
lisi.sayName() // I am Li Si
函数的call()和apply()方法
函数的call()方法是另一种形式的函数调用,例如:
a.b(1,2)等价于b.call(a,1,2)。例如Symbol.iterator.call(arr)。
apply()与call()相比只是传参的形式不同。a.b(1,2)等价于b.a([1,2])。
数组
新建数组
有多种方式新建数组。
第一种方式是使用Array()构造函数,如下:
const arr1 = new Array() // 建立一个空数组
const arr2 = new Array(3) // 建立一个包含3个元素的数组
第二种方式是使用数组字面量,外层用中括号([])包裹,数组元素之间用逗号隔开,如下:
const arr1 = [] // 建立一个空数组
const arr2 = [1,2,3] // 建立一个数组,包括3个元素
第三种方式是使用Array.from()静态方法,该方法接收一个可迭代对象,例如:
const str = 'hello' // 字符串是可迭代对象
const arr1 = Array.from(str)
console.log(arr1) // ['h','e','l','l','o']
const set = new Set(1,2,3) // 集合是可迭代对象
const arr2 = Array.from(set)
console.log(arr2) // [1,2,3]
第四种方式是使用Array.of()静态方法,该方法与Array.from()类似,区别是Array.of()接收若干个元素作为参数组成新数组的元素,例如:
const arr = Array.of(1,2,3)
console.log(arr) // [1,2,3]
第五种方式是使用三点运算符(…),该方法可以看作是Array.from()方法的语法糖,接收一个可迭代对象,通常用于快速将字符串转化为数组,例如:
const str = 'hello'
const arr = [...str]
console.log(arr) // ['h','e','l','l','o']
当然,还有许多方法可以新建或返回新数组,比如:Object.keys()、Object.values()、Object.entries(),这将在后面会讲到。
数组的长度
数组的长度不是固定的,哪怕一开始指定数组的长度,其长度也是可以动态伸缩的。例如:
const arr= new Array(3)
arr[3] = 1
console.log(arr) // 4
数组元素的默认值
如果没有给定数组的某个元素的具体值,那么该元素就会被赋予默认值undefined,例如:
const arr = new Array(3)
console.log(arr[0]) // undefined
数组的拖尾逗号
有些时候,我们会看到数组元素的结尾也存在一个逗号,这种逗号叫做拖尾逗号,通常是为了在频繁地增减数组元素的时候同时保证语法的正确,拖尾逗号不占用数组的长度,例如:
// 下面两种写法的结果是一样的
const arr1 = [1,2,3]
const arr2 = [1,2,3,]
console.log(arr1.length, arr2.length) // 3 3
数组的实例方法
Array.prototype.fill()
数组的fill()可以让数组在指定的索引范围内填入相同的值,该方法接收三个参数: 要填充的值、起始索引(默认为0)、终止索引(默认为最后一个元素,可以用负整数表示倒数)。
fill()方法会修改源数组,来看下面的例子:
const arr1 = [1,2,3,4,5,6]
const arr2=[...arr1]
const arr3=[...arr1]
arr1.fill(20)
console.log(arr1)
// [20,20,20,20,20,20]
// => 所有位置都填充为20
arr2.fill(20,2)
console.log(arr2)
// [1,2,20,20,20,20]
// 从第3个元素开始,一直到结尾
arr3.fill(20,1,-2)
// [1,20,20,20,5,6]
// 从第2个开始(包含),到倒数第二个结束(不包含)
Array.prototype.flat()
有些场景下,我们需要将具有嵌套结构的数组打平,ES2019新增了flat()方法用于数组的打平操作,例如:
const arr1= [ 1, [2,3],4 ]
const arr2=arr.flat()
console.log(arr2) //=>[1,2,3,4]
该方法还可以接收一个整数,表示打平的深度,默认情况下,打平一级嵌套,例如:
const arr=[[[1]]] // 三级嵌套
const arr1=arr.flat()
console.log(arr1) //=> [[1]]
const arr2=arr.flat(2)
console.log(arr2) //=>[1]
注意,当打平深度高于嵌套层级时,永远只会返回一维数组:
// const arr=[[[1]]]
const arr3=arr.flat(6)
console.log(arr) //=>[1]
也就是说,打平的结果永远还是数组。
Array.prototype.at()
ES2019新增了at()
方法,该方法可以从倒数第一位开始访问数组元素,以往我们要找到数组的最后一个元素,使用的是:
const arr = [1,2,3]
console.log(arr[arr.length-1]) // 3
现在,有了at()
方法,便可以非常方便地返回最后一个元素值:
const arr = [1,2,3]
console.log(arr.at(-1)) // 3
通过索引返回元素,at()
方法比中括号形式([])更加通用,因为at()
可以接收一个正整数或负整数,正整数就是正序索引,等同于中括号形式([]),而负整数就是逆序索引,例如:
const arr = [1,2,3]
console.log(arr.at(0)) // 1 , 等价于arr[0]
console.log(arr.at(-1)) // 3 , 返回倒数第一个元素值
console.log(arr.at(-2)) // 2 , 返回倒数第二个元素值
在数组首尾增加/删除元素
要在数组首尾插入/删除元素,有四种情况:
- push() :在数组尾部增加任意数量的元素,并选择性返回数组的新长度。
- pop(): 删除数组的最后一项,并选择性返回刚刚删除的最后一项。
- shift() :删除数组的第一项,并选择性返回刚刚删除的第一项。
- unshift():在数组开头添加任意数量的元素,并选择性返回数组的新长度。
上述"选择性返回"的意思是可以接收返回值,也可以不接收。例如:
const arr1 = [1,2,3]
arr1.push(4) // 不接收返回值,此时arr1=[1,2,3,4]
const arr2 = [4,5,6]
console.log(arr.push(7)) // 4 ,接收返回值,返回数组的新长度,此时arr2 = [4,5,6,7]
对数组的首尾进行增减元素的示例如下:
const arr =[2,3]
arr.push(4) // 在尾部压入一个新元素,此时arr = [2,3,4]
arr.push(5,6,7) // 在尾部一次性压入3个新元素,此时arr=[2,3,4,5,6,7]
arr.pop() // 从尾部弹出最后一个元素,此时 arr = [2,3,4,5,6]
arr.unshift(1) //从首部压入一个新元素,此时arr= [1,2,3,4,5,6]
arr.unshift(-3,-2,-1,0) // 在首部一次性压入4个新元素,此时 arr= [-3,-2,-1,0,1,2,3,4,5,6],注意压入的顺序
arr.shift() // 从首部弹出第一个元素,此时arr =[-2,-1,0,1,2,3,4,5,6]
console.log(arr) // [-2,-1,0,1,2,3,4,5,6]
Array.prototype.join()
可以使用join()将数组元素用指定的符号拼接起来,再转换为字符串,例如:
const arr = [1,2 ,3, 4 ]
const str = arr.join('&')
console.log(str) // '1&2&3&4'
如果没有给定符号,则默认使用英文逗号拼接(,),例如:
const arr = [1,2 ,3, 4 ]
const str = arr.join() // 等价于 arr.join('') 和 arr.join(',')
console.log(str)
注意,在开始拼接之前,每个元素会先隐式调用toString()方法转成字符串,然后用给定的符号拼接。例如:
const arr = [1,2,{a:1},3]
const str = arr.join()
console.log(str) // '1,2,[object Object],3'
在上面的代码中,元素{a:1}是Object类型,任意Object类型使用toString()均会返回字符串’[object Object]',所以会打印出上面的结果。
Array.prototype.slice()
数组实例的slice()用于返回一个子数组,也叫数组切片,该方法接收两个参数:
- 起点的索引位置,包含
- 终点的索引位置,不包含
例如:
const arr1 = [1,2,3,4,5,6]
const arr2 = arr1.slice(1,3)
console.log(arr2) // [2,3]
有一种简便的记忆方法,slice(n,m)返回第 n+1 到第 m 个元素组成的新数组。
Array.protype.includes()
仅仅是查看是否包含某个元素,可使用Array.prototype.includes()方法,该方法返回一个布尔值,例如:
const arr = [1,2,3]
console.log(arr.includes(1)) // true
console.log(arr.includes(5)) // false
Array.prototype.indexOf() 、 Array.prototype.lastIndexOf()
如果不仅要看是否包含某个元素,还要找出第一次出现的位置,则应该使用Array.prototype.indexOf()方法,如果能找到,则返回第一次出现的索引位置,如果没有,则返回-1。如果要返回最后一次出现的索引位置,则使用Array.prototype.lastIndexOf(),例如:
cosnt arr = [1, 2, 3, 4, 2, 5]
const result1 = arr.indexOf(2) // 第一次出现2的索引位置
console.log(result1) // 1
const result2= arr.indexOf(6) //第一次出现6的索引位置,没有找到
console.log(result2) // -1
const result3 = arr.lastIndexOf(2) // 最后一次出现2的索引位置
console.log(result3) // 4
请注意indexOf()和lastIndexOf()只能找到第一次和最后一次出现的位置,如果需要将所有的位置都找到,则应该使用filter()方法。
find() 、findeIndex()
上面的查找方法只能查看是否包含特定的元素,如果要查找符合某些要求的元素,则需要使用Array.prototype.find()和Array.prototype.findIndex()方法。find()方法返回第一个匹配的元素,findIndex()方法返回第一个匹配的元素的索引位置。
这两个方法都接收一个callback函数,该函数接收三个参数:元素、索引位置、数组本身。例如:
const arr = [1, 2, 3, 4, 5, 6 ]
const result1 = arr.find(item => item%2==0) // 返回第一个偶数
console.log(result1) // 2
const result2 = arr.findIndex(item => item%2==0) // 返回第一个偶数的索引位置
console.log(result2) // 1
find()和findIndex()在找到第一个匹配的元素之后就不再往后迭代,因此,倘若要找到全部匹配的元素,则应该使用filter()方法。
数组的迭代
数组有五个迭代函数,它们都接收一个函数作为参数,传入的函数接收三个参数:元素、索引位置、数组本身。这五个迭代函数分别是:
- Array.prototype.map():返回对每个元素进行操作后的新数组。
- Array.prototype.filter():返回回调函数返回值为true的元素组成的新数组。
- Array.prototype.every():如果回调函数返回值均为true,则返回true,否则返回false。
- Array.prototype.some():只要有一个或以上的回调函数返回值为true,则返回true,否则返回false。
- Array.prototype.forEach():不返回新数组,而是直接在原来的数组上对每个元素执行回调函数定义的操作。
来看几个例子:
const arr = [1, 2, 3, 4, 5, 6, 7, 8, 9 ]
const arr2 = arr.map((element,index,array)=>element*2)
// 对每个元素乘以2,存储到新数组中,arr2=[2,4,6,8,10,12,14,16,18]
const arr3 = arr.filter((element,index,array)=>element > 5)
// 找出大于5的值,存储到新数组中,arr3=[6,7,8,9]
const arr4=arr.every((element,index,array)=>element%2==0)
// 所有元素都是偶数吗?显然不是,arr4 = false
const arr5=arr.some((element,index,array)=>element%2==0)
// 存在元素是偶数吗?arr5=true
arr.forEach((element,index,array)=>element**2)
// 直接修改原数组,对每个元素进行平方,arr=[1,4,9,16,25,36,49,64,81]
数组元素的排序
对数组排序要用到两个函数:.sort()和reverse(),这两个函数本质一样,只是一个正序一个倒序。sort()函数接收一个callback函数作为参数,该函数只需要给出谁大谁小的定义逻辑即可,该函数接收两个值,需要返回正数、0、负数表示排序谁排在前面。这两个函数都会直接修改原数组。
需要特别提醒的是,如果不给sort()传入排序函数,sort()会按照字符串的形式升序排序,哪怕所有元素都是数字,例如:
const arr = [1,3,11,222,2]
arr.sort() )
console.log( arr ) //=> [1,11,2,222,3],这显然不符合我们的预期
来继续看下面几个例子。
对元素均为字符串的数组进行升序排序:
const arr = ['zhang san', 'li si', 'wang wu','zhao liu']
arr.sort( )
console.log(arr) // => ['li si', 'wang wu', 'zhang san', 'zhao liu'] ,默认会按照元素的字符进行排序
对元素均为数值的数组进行排序,默认为升序:
const arr = [1,3,11,222,2]
arr.sort( (value1,value2)=>value1-value2 )
console.log(arr) //=> [ 1, 2, 3, 11, 222 ]
这个例子让参数作减法,返回正数、负数以决定排序逻辑。
在实际应用中,并不是都只对数值或字符串进行排序,比如下面这个例子,无论是什么数组,都应该按照赵、钱、孙、李、周、吴、郑、王的顺序排列:
const str = "赵、钱、孙、李、周、吴、郑、王"
const standard = str.split('、')
const arr = ['王' , '李' ,'周', '赵' ,'吴','钱','郑','孙' ]
arr.sort ( (value1,value2) => standard.indexOf(value1)-standard.indexOf(value2))
console.log(arr)
这个例子使用了数组的indexOf()方法,通过在标准数组中查询索引,再将索引作减法,以确定谁排在前面。
数组的归并
归并操作是对数组的元素进行叠加运算,例如累加或累积,涉及reduce()和reduceRight(),这两个方法的区别只是叠加运算的方向相反。
reduce()方法接收一个函数参数,这个函数与前面的函数参数有所不同,它期待4个参数:
- prev:叠加运算的初始值,默认为第一项
- cur:迭代的当前元素,第一次迭代为第二项
- index:索引位置
- array:数组本身
来看下面的示例:数组累加求和。
const arr = [1, 2, 3, 4]
const result = arr.reduce( (prev, cur)=> prev + cur )
console.log(result)
该归并操作的细分步骤如下:
第一次归并:prev=1,cur=2
第二次归并:prev = prev+cur=1+2=3,cur = 3
第三次归并:prev=prev+cur=3+3=6 ,cur=4
第四次归并:prev=prev+cur=6+4=10
数组的本质也是对象
现在,我们来探讨一个问题——数组的本质。
我们知道,数组的索引使用中括号,中括号里面是数字,表示元素的序号,从0开始,例如:
const arr = [1,2,3]
console.log(arr[0]) // 1
这种中括号的访问方式类似于对象的属性访问。我们知道,对于是字面量的属性名称,对象属性的访问需要使用引号,将这种规则加到数组上试试:
const arr = [1,2,3]
console.log(arr["0"]) // 1
依然能够找到第一个元素。
这就说明,数组的本质也是对象,只不过基于对象做了一些特殊处理,包括:
-
默认情况下,数组元素的属性名是整数,且从0开始整数递增。
-
数组不能使用点号语法访问元素值,例如arr.0是错误的。
既然属性的本质是对象,那么除了只能是数值的默认属性之外,也可以添加一些自定义属性,例如:
const arr = [1,2,3]
arr['key1'] = 'hello'
arr['key2'] = 'world'
console.log(arr) // [ 1, 2, 3, key1: 'hello', key2: 'world' ]
console.log(arr.key1) // hello, 对象的点号访问语法
console.log(arr['key2']) // world, 对象的中括号加引号访问语法
concat()
使用concat()方法可以合并数组,返回有原数组和实参组成的新数组:
const arr1 = [1, 2, 3]
const arr2 = [4, 5, 6]
const arr3 =arr1.concat(arr2)
console.log(arr3) // [1,2,3,4,5,6]
当然,也可以使用扩展运算符更便捷的操作:
cosnt arr1 = [1, 2, 3]
const arr2 = [4, 5, 6]
const arr3=[...arr1 , ...arr2 ]
console.log(arr3)//[1,2,3,4,5,6]
需要特别注意的是,这两种方法只有在所有元素均为原始值的前提下,新数组与源数组是相互独立的。如果源数组中存在引用值元素,那么新数组与源数组同时关联着这些引用值元素。
这是由引用值自身的特性决定的,为了保险起见,建议只对所有元素都为原始值的数组使用这两种操作。
splice()
splice()方法可以在任意位置对数组插入、删除元素。一个完整的splice()方法依次接收如下参数:
- 插入或删除的起点索引位置
- 要删除的元素个数,如果不指定,那么从起点位置开始的后面的所有元素都被删除
- 后面的参数为要从起点位置之后开始填充的元素
例如,arr.splice(n,m,x,y)表示从arr的第n+1个元素开始删除m个元素,然后在此处插入x、y元素。
需要注意的是,splice()方法直接修改源数组,而不是返回新数组,来看下面的例子:
const arr=[1,2,3,4,5,6]
arr.splice(1,3,20,30,40)
console.log(arr) // [ 1, 20, 30, 40, 5, 6 ]
// 从第2个位置开始删除3个元素,然后在第2个位置之后填充20,30,40
对象
新建对象实例
Object是JavaScript中最常见的数据类型,也是其它引用类型的基类。一个对象实例由一个或多个名 / 值对组成。
创建对象的方法有多种,第一种是使用对象字面量的方式新建一个对象实例:
const obj = {a:1,b:2}
第二种方式是使用new Object()创建对象,如下:
const obj = new Object({a:1, b:2})
第三种方式是使用Object.fromEntites()创建对象,这个方法接受一个可迭代对象,例如一个二维数组:
const arr = [ ['a',1],['b',2] ]
const obj = Object.fromEnties(arr)
console.log(obj) // {a:1, b:2}
也可以接收一个Map类型的实例,例如:
const map = new Map([ ['a',1],['b',2] ])
const obj = Object.fromEnties(map)
console.log(obj) // {a:1, b:2}
第五种方式是读取一个JSON字符串创建对象,该字符串包裹的是一个对象字面量,也可以是通过Node.js的fs模块的readFileSync()方法从本地某个.json文件读取的。
通过JSON字符串创建对象的示例如下所示:
const str = '{'a':1,'b':2}'
const obj = JSON.parse(str)
console.log(obj) // {a:1, b:2}
对象的属性名和属性值
使用点号可以访问或者设置对象的属性值:
const obj = {a:1,b:2}
console.log(obj.a)
obj.a=2
comsole.log(obj)
除了点号,还可以使用中括号访问或设置对象的属性值,注意需要对属性名使用引号:
const obj = {a:1,b:2}
console.log(obj["a"])
以上两种方式适用于属性名是字面量的情况下,如果属性名是个变量标识符,那么只能使用中括号,且不能加引号,例如:
const key1='a'
const key2='b'
const obj = {}
obj[key1]=1 // 解析为obj["a"]=1
obj[key2]=2 // 解析为obj["b"]=2
对象的属性值也可以是变量,例如:
const a=1
const b=2
const obj={a:a,b:b}
// 解析为 const obj={a:1,b:2}
对象的成员不仅可以是变量,也可以是函数。按照习惯,在对象中,变量被称呼为属性,函数被称呼为方法。例如:
const obj = {
a:1 ,
b:2,
c:function(){
console.log('hello,world')
}
}
在上面的代码中,对象实例obj有两个属性a和b,有一个方法c()。
对象的简写特性
EcamsSript 6为对象新增了简写特性,这并没有改变对象本身的行为,但极大地提升了编码和阅读的效率。
上面这个例子演示的是属性值是变量的情况,但是又有一个特征,就是:属性名和属性值的标识符是一样的,例如:
const a=1
const b=2
const obj={a:a,b:b}
console.log(obj) // {a:1, b:2}
这时候,就可以使用简写方式:
const a=1
const b=2
const obj={a,b} // 等价于 const obj = {a:a, b:b}
console.log(obj) // {a:1, b:2}
除了属性可以简写以外,方法也有简写的方式,就是去掉冒号和function关键字,例如:
const obj = {
a:1,
b:2,
sum(){
return a+b
}
}
console.log(obj.sum())
在有些场景中,可以看到对象的最后一个属性值后面还留有一个逗号,这种逗号叫做拖尾逗号。拖尾逗号可用在需要经常增加、删除对象属性的情况,可以保证每次操作的一致性,避免发生低级的语法错误。
不过,要特别注意的是,JSON格式不支持拖尾逗号。
对象属性的读取和赋值
通常情况下,对象的属性名是明确的字面量,这时候时候点号选取对象属性,读取或写入属性的值,例如:
const obj = {a:1, b:2}
console.log(obj.a)
obj.a = 2
console.log(obj.a)
使用点号可以连续读取属性,例如:
const obj = {a:{a:1,b:2}, b:2}
console.log(obj.a.a)
这里的两个属性名a并不冲突,因为它们从属于不同的对象命名空间。
可选链(?.)
如果对象obj不存在属性c,则obj.c返回undefined。而如果继续对obj.c读取属性,例如obj.c.d,则此时会报错,使用EcmaScript 2020新增的可选链(?.)特性, 可以解决这个报错的问题。如下示例:
const obj = {a:1,b:2}
console.log(obj.c) // undefined
console.log(obj.c.d) // 报错,因为obj.c为undefined
console.log(obj?.c?.d) // 存在属性则返回,不存在则会返回undefined,而不会报错
可选链加强了程序的健壮性,无需开发者手动处理潜在的报错问题。
对象的可计算属性
对象的可计算属性是ES6新增的特性。有些情况下,属性名是一个变量,无法使用点号语法得到属性值,此时可以使用方括号的方式读取属性,例如:
const key1 = 'a'
const key2 = 'b'
const obj = {}
obj[key1] = 1
obj[key2] = 2
console.log(obj) // {a:1, b:2}
使用中括号的优势是可以通过变量访问属性。
对于一个确定的属性名称,除了使用点号外,也可以使用中括号读取属性,但此时需要使用引号:
const obj = {}
obj['a'] = 1
obj['b'] = 2
console.log(obj) // {a:1, b:2}
对象的拖尾逗号
一个对象往往有多个名/值对,各个名/值对之间使用逗号隔开,需要说明的是,最后一个名/值对后面的逗号也是允许的,并不会报错:
const obj = {
a:1,
b:2,
c:3,
}
这个宽松的语法特性在需要频繁复制粘贴追加的属性时非常有用,因为格式是统一的,我们不需要频繁的增减逗号。
不过,要特别说明的是,JSON的写法类似于对象,不过,JSON的写法与对象有两个最大的不同:
-
JSON字符串中的对象的属性名必须加引号。
-
JSON字符串中,对象不允许使用拖尾逗号,使用会报错。
对象属性名的简写
在很多时候,对象的属性值是一个变量标识符,而这个标识符和属性名是一样的,例如:
const a = 1
const b = 2
const obj = {a:a, b:b}
console.log(obj) // {a:1, b:2}
这种情况下,可以使用一种简化的语法,如下:
const a = 1
const b = 2
const obj = {a , b}
console.log(obj) // {a:1, b:2}
对象方法的简写
对象往往具备多个方法,方法其实就是函数,只不过在对象的命名空间中我们称之为方法,例如:
const obj = {
a:1,
b:2,
say:function(){
console.log('Hello,World!')
}
}
obj.say()
此时可以省略方法名称后面的冒号和function关键字:
const obj = {
a:1,
b:2,
say(){
console.log('Hello,World!')
}
}
obj.say()
原型链
JavaScript最最初就支持类的定义,不过,ES6之前是使用构造函数的方式,ES6正式支持使用class关键字定义一个类。这两种方式的底层原理是相同的,都是基于原型的继承。
多个实例往往需要共享一些方法,因此我们定义一个类,作为多个实例的构造器,每个实例都使用这个命名空间中的成员。例如,通过Array构造函数实例化了arr1和arr2,我们就说arr1和arr2都继承了Array。Array默认存在一个共享空间,供实例调用,这个共享空间就是实例的原型,默认为构造函数或类的prototype属性的值,即Array.prototype。
类本质上是一个命名空间,包含两个空间:
- 静态成员,可以直接被类调用,例如Array.from()、Object.values()。
- prototype对象,这个对象里面的成员是供实例使用、供子类继承的。例如Array.prototype.forEach()。
虽然不是ES标准,但是大多数浏览器都为实例或子类提供了__proto__属性,该属性的值有两种情况:
- 实例对应的类的prototype对象
- 子类的prototype对象对应的父类的prototype对象
例如:
console.log([].__proto__===Array.prototype)
// =>true,空数组实例对应的类是Array
console.log(Array.prototype.__proto__===Object.prototype)
//=> true,Array的父类是Object
console.log(Object.prototype.__proto__===null)
//=> true,Object的父类是null
Object的父类是null,这只是标准上的规定,我们需要知道的是,所有类型的起点都源于Object。
我们将上面两个操作串联起来,就形成了一条原型链:
console.log([].__proto__.__proto__===Object.prototype)
由此我们可以得出JavaScript的继承逻辑:Object是所有引用类型的继承起点,Object生出Array、Map、Set、Function等类型,再由这些类型生成实例。这些实例拥有丰富的方法,是因为可以通过原型链往上追溯,直到追溯到Object。
因此,要全面了解一个数据类型,从三方面入手:
- 看静态成员
- 看prototype对象中有哪些方法提供给了实例
- 通过<类名>.prototype.proto.proto…不断往上追溯,寻找更丰富的方法,提供给实例使用
拿Array类型举例,从三方面入手:
- 看静态成员,有Array.from()、Array.of()、Array.isArray()等静态方法
- 看Array.prototype,有Array.prototype.length、Array.prototype.sort()、Array.ptototype.splice()、Array.prototype.forEach()等诸多方法供数组实例使用。
- 往上追溯,有Object.prototype.toString()、Object.prototype.toString()等方法,这些方法也可以被数组实例使用。
另外要提醒的是,追溯的过程遵循就近原则,从实例本身开始追溯,如果已经找到了成员,那么就会直接使用该成员,而不再继续追溯。
最后要强调的是,不要修改内置类型的prototype对象,也不要修改默认的__proto__指向,这些都是“玩火”行为。实际上,JavaScript语言规定的原型链是让我们去使用的,不是让我们去修改的,在绝大部分情况下,我们用好实例及其API就足够了。JavaScript不是偏向面向对象的语言,其更多侧重于函数式编程。对于原型和原型链这个知识点,我们一定要深入理解,但却没有多大的必要去使用。
对象的for in 方法
可迭代对象可以使用for of 循环遍历,而Object类型并非可迭代对象,不过可以使用for in 方法遍历其属性名和属性值,例如:
const obj = {a:1, b:2, c:3}
for (let key in obj){
console.log(key) // a b c
}
从这里可以看出,如果只有一个参数,那么只遍历属性名称。
对象的枚举
对象是一组名/值对,可以使用如下方法枚举属性名、属性值、名/值对:
const obj = {a:1, b:2, c:3}
const keys = Object.keys(obj)
console.log(keys) // [ 'a', 'b', 'c' ]
const values = Object.values(obj)
console.log(values) // [ 1, 2, 3 ]
const entries = Object.entries(obj)
console.log(entries) // [ [ 'a', 1 ], [ 'b', 2 ], [ 'c', 3 ] ]
对象的属性特性
对象的属性的特性属于比较复杂但不是很难的知识点。
对象的成员分为属性和方法,而对象的属性又分为:
-
数据属性
-
访问器属性
对象的数据属性
一般情况下,对象的属性就是数据属性,例如:
const obj = {a:1,b:2}
这里,a、b均为数据属性。
对象的数据属性有4个特性:
-
value:表示该属性的值
-
writable:表示该属性的值是否可以被修改
-
enumerable:表示该属性是否可以通过for-in 循环遍历,默认情况下为true,如果将该特性设置为false,则for-in循环时将遍历不到该属性。
-
configurable:表示是否可以通过delet删除该属性,或者是否可以修改其特性。
这里要注意的是writable和configurable的区别,writable侧重于能不能修改属性的值,而configurable侧重于能不能配置该属性的特性。
对象的属性的特性使用Object.defineProperty()方法来定义:
const obj = {}
Object.defineProperty(obj,'a',{
value:1,
writable:false, // 不能修改该属性的值
enumerable:true,
configurable:true
})
console.log(obj.a) // 1
obj.a = 2
console.log(obj.a) // 1 : 不会报错,但是静默失败
对象的访问器属性
对象的访问器属性有4个特性:
-
get:获取函数,在读取该属性时调用。
-
set:获取函数,在写入属性时被调用。
-
enumerable:与数据属性的作用相同。
-
configurable:与数据属性的作用相同。
这里的难点是get和set,如果对属性只定义了get函数,那么该属性就是只读的,必须同时定义get和set函数才说明该属性是可写的,例如:
const obj = {a:1}
Object.defineProperty(obj,'b',{
get(){return this.a},
set(value){this.a = value}
})
console.log(obj.b) // 1
obj.b = 2
console.log(obj.a) // 2
这里a是数据属性,b是a的访问器属性,可读可写。再来看一个只读访问器的例子:
const obj = {a:1}
Object.defineProperty(obj,'b',{
get(){return this.a},
})
console.log(obj.b) // 1
obj.b = 2
console.log(obj.a) // 1 : 不会报错,但是静默失败
合并对象
使用Object.assign()合并对象
可以使用Object.assign()合并对象,例如:
const obj1 = {a:1, b:2}
const obj2 = {a:2, c:3}
const obj = Object.assign(obj1,obj2)
console.log(obj) // { a: 2, b: 2, c: 3 }
合并对象时,如果存在同名属性,则后边的对象属性值会覆盖前面的属性值。
使用三点操作符合并对象
作为一种语法糖,可以使用三点操作符合并对象:
const obj1 = {a:1, b:2}
const obj2 = {a:2, c:3}
const obj = {...obj1,...obj2}
console.log(obj) // { a: 2, b: 2, c: 3 }
最后,要特别说明的是,无论使用Object.assign(),还是使用三点运算符,只推荐源对象不包括嵌套属性、并且属性值是原始值的时候使用,此时新对象对于源对象是独立的,不存在深浅拷贝的问题。如果源对象包括嵌套属性、或者属性值存在非原始值(其实嵌套属性本身也意味着属性值非原始值了),那么新对象的某些属性可能还引用着源对象,这里面有一些“语法陷阱”需要避免,建议先使用其它方式将对象打平,再进行合并。
对象的toString()和valueOf()
toString()
所有对象实例的toString()方法会返回一个固定的字符串[object Object],例如:
console.log({}.toString()) // [object Object]
console.log({a:1, b:2}.toString()) // [object Object]
当对象与对象相加,对象与字符串相加时,会隐式调用toString(),如下:
console.log({}+{}) // [object Object][object Object]
console.log(({}+{}).length) // 30
console.log({}+'Hello') // [object Object]Hello
valueOf()
对象示例的valueOf()返回对象本身,例如:
console.log({}.valueOf()) // {}
console.log({a:1, b:2}.valueOf()) // { a: 1, b: 2 }
对象的枚举
对象是一组名/值对,可以使用如下方法枚举属性名、属性值、名/值对:
const obj = {a:1, b:2, c:3}
const keys = Object.keys(obj)
console.log(keys) // [ 'a', 'b', 'c' ]
const values = Object.values(obj)
console.log(values) // [ 1, 2, 3 ]
const entries = Object.entries(obj)
console.log(entries) // [ [ 'a', 1 ], [ 'b', 2 ], [ 'c', 3 ] ]
JSON
JSON全称是JavaScript对象表示法,是通用的数据交换格式,许多软件的配置文件均使用JSON文件格式。
要特别说明的是,JSON不是对象,JSON就是字符串,JSON字符串可以包括三种语法:
-
原始值
-
对象
-
数组
将对象转换为JSON
要将对象转换为JSON字符串,使用JSON.stringify(),例如:
const obj = {a:1, b:2, c:3}
const json = JSON.stringify(obj)
console.log(json) // {"a":1,"b":2,"c":3}
可以在第二个参数中指定一个数组,表示筛选哪些属性进入JSON字符串:
const obj = {a:1, b:2, c:3}
const json = JSON.stringify(obj,['a','c'])
console.log(json) // {"a":1,"c":3}
将JSON序列化为对象
可以将JSON序列化为对象,例如:
const json = '{"a":1,"b":2,"c":3}'
const obj = JSON.parse(json)
console.log(obj)
再来看一个例子,从本地的配置文件中读取JSON字符串转化为对象,修改后保存回配置文件。
如下是配置文件settings.json的内容:
{
"a":1,
"b":2,
"c":3
}
如下读取配置文件并解析为对象,然后写回配置文件中:
const fs = require('fs')
const json = fs.readFileSync('settings.json','utf8')
const obj = JSON.parse(json)
console.log(obj) // { a: 1, b: 2, c: 3 }
obj.a = 2
obj.c = 5
const json2 = JSON.stringify(obj)
fs.writeFileSync('settings.json',json2,'utf8')
map和set
map基础
Map类型是ES6新增的集合引用类型,对于强调键值映射和迭代的操作来说,Map类型比Object类型更加实用。Map的优势在于:
- 是可迭代对象,这意味着不需要像Object那样使用for-in循环来遍历元素。
- 更加方便的增删改查操作。
使用构造函数新建一个空Map:
const map = new Map()
要在新建的时候同时填充内容,可以使用set链式操作:
const map =new Map().set('a',1).set('b',2)
除此之外,new Map()方法接收一个二维数组作为新Map实例的键值对:
const map =new Map([ ['a',1],['b',2] ])
因此,可以使用Object.entreis()静态方法将对象的元素填充进map:
const obj = {a:1,b:2}
const map=new Map(Object.entries(obj))
打印map时,输出结果是这样的:
const map =new Map().set('a',1).set('b',2)
console.log(map)
// => Map(2) { 'a' => 1, 'b' => 2 }
map实例的操作
要增加或修改Map实例的键值,使用set()方法,允许链式操作,如果set()方法中的键名已在map中存在,那么就会修改键对应的值,否则就是增加键值,例如:
const map =new Map([ ['a',1],['b',2] ])
map.set('b',3) //修改键对应的值
map.set('c',3) // 增加键值
map.set('d',4).set('c',5).set('f',6) //链式操作
console.log(map)
//=> Map(6) { 'a' => 1, 'b' => 3, 'c'=>3,'d'=>4,'e'=>5,'f'=>6}
使用has()方法可以查询map是否存在某个键:
// 承接上文的map
map.has(' a ') // => true
map.get( ' aa ' ) // => false
使用get()方法可以通过键查询对应的值,如果键不存在,则返回undefined:
// 承接上文的map
map.get('a') // 1
map.get('aa') // undefined
使用delete()方法删除map中的键,删除成功返回true,删除不成功(键不存在)则返回false,例如:
// 承接上文的map
map.delete('f') // =>true
map.delete('f') // =>false,键已经在上一步被删除了
要取得map的键值对的个数,使用size属性:
// 承接上文
console.log(map.size) // =>5
如果要清空map中所有的键值对,使用clear()方法,例如:
// 承接上文
map.clear()
console.log(map.size) // 0
console.log(map) // Map(0) {}
map到数组的转换
对map实例使用下面三种方法,可以返回可迭代对象:
- keys(),返回由键组成的新数组
- values(),返回由值组成的新数组
- entries(),返回由键值组成的二维数组
例如:
const map = new Map().set('a',1).set('b',2).set('c',3)
console.log(map.keys()) //=>[Map Iterator] { 'a', 'b', 'c' }
console.log(map.values())// => [Map Iterator] { 1, 2, 3 }
console.log(map.entries())// => [Map Entries] { [ 'a', 1 ], [ 'b', 2 ], [ 'c', 3 ] }
使用Array.from()或者[…iterator],就可以将上面几个可迭代对象转换为真正的数组:
// 承接上文
console.log([...map.keys()])// => [ 'a', 'b', 'c' ]
console.log([...map.values()]) // => [ 1, 2, 3 ]
console.log([...map.entries()]) // => [ [ 'a', 1 ], [ 'b', 2 ], [ 'c', 3 ] ]
set
Set数据类型类是ES6新增的集合引用类型,表示元素唯一的集合。结构上类似于数组,与数组的区别是Set的元素不能重复。
可以使用构造函数新建一个空的Set实例:
const set = new Set()
该函数也接收一个可迭代对象:
const set = new Set([1,2,3])
打印一个set实例会输出如下结果:
const set = new Set([1,2,3]
console.log(set) //=> Set(3) { 1, 2, 3 }
set实例的size属性返回set的元素个数:
const set = new Set([1,2,3])
console.log(set.size)
使用add()方法添加元素,可使用链式操作:
const set = new Set().add(1).add(2).add(3)
要查询是否包含某个元素,使用has()方法,该方法返回一个布尔值:
const set = new Set([1,2,3])
console.log(set.has(2))
console.log(set.has(6))
要删除某个元素,使用delete()方法,可以选择接收返回值,返回值是一个布尔值,表示是否已删除成功:
const set = new Set([1,2,3])
set.delete(1)
console.log(set.delete(2)) //=>true
console.log(set.delete(2)) //=> false
要清空set的所有严元素,使用clear()方法:
const set = new Set([1,2,3])
set.clear()
console.log(set.size) //=>0
迭代
可迭代对象
对于某种数据类型,如果它的元素可以按照确定的顺序进行有限的读取,那么我们认为这个数据类型是可迭代对象。可迭代对象有两大关键特征:元素的数量是有限的;元素的枚举顺序是确定的。因此,数组、Map、Set都是可迭代对象。要特别注意两种类型:string虽然是原始类型,但是可以被当成可迭代对象使用,里面的字符是它的元素;Object类型不是可迭代对象,因为Object的元素的枚举顺序被设计为不确定的。
可迭代对象允许使用for-of枚举元素以及使用[… iterator]进行扩展操作。
可迭代协议和迭代器协议
数组、Map、字符串之所以能够进行for-of操作,是因为它们都实现了可迭代协议。可迭代协议指的是一个对象实现了Symbol.iterator方法,调用这个方法会返回一个迭代器。
对上面返回的迭代器多次调用next()方法,会不断返回一个迭代结果对象,该对象包含两个属性:表示枚举的元素值以及表示是否已经枚举完毕的布尔值。能够进行这种操作,是因为实现了迭代器协议。
这里有必要澄清三个概念:可迭代对象、迭代器、迭代结果对象。一个实现了Symbol.iterator方法的、能够有序有限遍历元素的对象是可迭代对象。可迭代对象调用Symbol.iterator方法会返回一个迭代器。迭代器多次调用next()方法会多次返回迭代结果对象。
从可迭代对象生成迭代器
一个可迭代对象调用symol.iterator方法会返回一个迭代器,例如:
const arr = [1,2,3,4]
const iter1 = arr[Symbol.iterator]()
console.log(iter1) // => Object [Array Iterator] {}
const str = 'hello'
const iter2 = str[Symbol.iterator]()
console.log(iter2) // => Object [String Iterator] {}
const map = new Map([['a',1],['b',1]])
const iter3 = map[Symbol.iterator]()
console.log(iter3) // => [Map Entries] { [ 'a', 1 ], [ 'b', 1 ] }
const set = new Set([1,2,3,4])
const iter4 = set[Symbol.iterator]()
console.log(iter4) // => [Set Iterator] { 1, 2, 3, 4 }
从迭代器生成迭代结果对象
对一个迭代器不断调用next()方法,会生成迭代结果对象。例如:
const arr = [1,2,3,4]
const iter = arr[Symbol.iterator]()
console.log(iter.next()) // => { value: 1, done: false }
console.log(iter.next()) // => { value: 2, done: false }
console.log(iter.next()) // => { value: 3, done: false }
console.log(iter.next()) // => { value: 2, done: false }
console.log(iter.next()) // => { value: undefined, done: true }
在上面的输出结果中,value表示迭代元素的值,done表示是否已经迭代完毕。
生成器
生成器是一个函数,调用该函数会返回一个迭代器。生成器使用function*声明,不过依然遵循函数的语法。在调用生成器函数时,该函数不会执行,而是返回一个迭代器。在生成器函数体中使用 yield 关键字可以不断回送迭代元素,例如:
function* generator(){
yield 1
yield 2
yield 3
}
const iter = generator()
console.log(iter) //=> Object [Generator] {}
console.log(iter.next()) //=>{ value: 1, done: false }
console.log(iter.next()) //=>{ value: 2, done: false }
console.log(iter.next()) //=>{ value: 3, done: false }
生成器使用 return 关键字终止回送迭代元素,例如:
function* generator(){
yield 1
yield 2
return 3
yield 4
}
const iter = generator()
console.log(iter) //=> Object [Generator] {}
console.log(iter.next()) //=>{ value: 1, done: false }
console.log(iter.next()) //=>{ value: 2, done: false }
console.log(iter.next()) //=>{ value: 3, done: true }
console.log(iter.next()) //=>{ value: undefined, done: true}
代理
ES6标准新增的代理与反射特性为开发者提供了拦截并向基本操作嵌入额外行为的能力。代理对象就是目标对象的抽象,代理对象本身是没有内容的,访问或修改代理对象都要先到达目标对象去读取或修改,然后将结果回送到代理对象上。使用new关键字调用Proxy()构造函数可以新建一个代理对象。该函数接收两个参数:目标对象和捕获器对象。捕获器对象中的每个方法有两种操作:一是增加一些额外的任务,二是默认阻止到目标对象的访问。
对代理对象的任何操作都会先在捕获器对象中寻找对应的方法进行执行。如果没有注册对应的方法,就会认为没有额外操作,也不会阻止代理对象到目标对象的访问。
虽然ES5的Object.defineProperty()方法也能实现类似的目标,但是它对于目标对象是破坏性的操作,而我们今天要说的代理是一种非破坏性的操作。
如果捕获器对象是一个空对象,那么代理对象和目标对象是实时同步的,如下:
const obj = {a:1}
const handler = {}
const proxy = new Proxy(obj,handler)
console.log(obj,proxy) // => { a: 1 } { a: 1 }
obj.a = 2
console.log(obj,proxy) // => { a: 2 } { a: 2}
proxy.a = 3
console.log(obj,proxy) // => { a: 3} { a: 3 }
一旦在捕获器对象种定义了某个操作对应的方法,那么对代理对象的操作就会被这个方法拦截。例如:
const obj = {a:1}
const handler = {
get(){console.log('Not Read')}
}
const proxy = new Proxy(obj,handler)
console.log(proxy.a) //=> 'Not Read'
这个例子中,proxy对象是obj对象的代理对象,在捕获器对象handler中,注册了get()方法。那么,当访问proxy对象的某个属性时,由于对应的get()方法被注册了,因此就只会执行该方法,该方法只打印了一个字符串,并不会输出属性a的值。
再比如:
const obj = {a:1}
const handler = {
set(){console.log('Read Only')}
}
const proxy = new Proxy(obj,handler)
proxy.a = 2 // => Read Only
console.log(obj,proxy) // => { a: 1 } { a: 1 } :并没有执行属性值的修改操作
这个例子中,在捕获器对象中定义了set()方法,该方法只输出了一个字符串,同时默认拦截了对目标对象属性值的修改。当修改proxy对象的属性值时,只会执行该方法,并不会将赋值操作传递到目标对象。
上面这个例子只是拦截并嵌入了额外行为,如果需要将对目标对象的操作传递到目标对象上,需要显示地指明:
const obj = {a:1}
const handler = {
set(target,name,value,proxy){
console.log('Set Operation')
target[name] = value //显示地对目标对象的修改
}
}
const proxy = new Proxy(obj,handler)
proxy.a = 2 // => Set Operation
console.log(obj,proxy) // => { a: 2 } { a: 2 }
在上面的例子中,target[name]= value可以替换成更实用的反射操作:Reflect.set(…arguments),表示将操作传导到目标对象上:
const obj = {a:1}
const handler = {
set(target,name,value,proxy){
console.log('Set Operation')
Reflect.set(...arguments)
}
}
const proxy = new Proxy(obj,handler)
proxy.a = 2 // => Set Operation
console.log(obj,proxy) // => { a: 2 } { a: 2 }
所以,如果不显式地修改目标对象或使用Reflect,那么访问或修改代理对象时,代理对象到目标对象的访问会被阻止,此时,修改代理对象的属性不会产生作用。
异步编程
期约基础
期约是为了简化异步编程而设计的语言特性。
使用new Promise()构造函数可以新建一个期约,该函数接收一个函数作为参数,我们先传入一个空函数:
const p=new Promise(()=>{})
console.log(p)
// => Promise { <pending> }
输出到结果表示期约的状态,目前为。
上面的函数参数又可以接收如下两个函数参数:
- resolve,可将期约的状态变为resolved
- reject,可将期约的状态变为rejected
例如:
const p=new Promise((resolve,reject)=>resolve())
console.log(p)
//=>Promise { undefined }
这个表示期约的值是undefined,如果没有状态,则状态为resolved。
现在,给定一个期约值:
const p=new Promise((resolve,reject)=>resolve(1))
console.log(p)
// Promise { 1 }
将期约变为rejected状态:
const p=new Promise((resolve,reject)=>reject(1))
console.log(p)
// => Promise { <rejected> 1 }
这个输出结果表示期约当前的状态是rejected,期约值是1。同时,此时控制台会输出一些错误信息,我们先不管它。
通过打印期约对象,我们可以发现,期约分为三种状态:
- pending
- resolved
- rejected
期约的状态决定了后续的操作是使用then()还是使用catch()。期约值是期约链式操作的数据流转的第一步。
除了构造函数以外,可以使用如下方式快速创建期约:
const p1= Promise.resolve(1)
console.log(p1)
// =>Promise { 1 }
const p2= Promise.reject(1)
console.log(p2)
//=> Promise { <rejected> 1 }
async
把函数声明为async意味着该函数的返回值将会是一个状态的期约:
async function 异步函数(){
return 1
// 等价于:
// return Promise.resolve(1)
}
console.log(异步函数())
// promise { 1 }
期约的实例方法和期约链
多层串联的回调函数本质上是多个函数的链式操作,上一个函数的返回值作为参数传递给下一个函数,使用期约对象的then()方法,就能实现链式操作,例如:
const p = Promise.resolve(1)
期约.then(x=>x+1)
.then(x=>x*2)
.then(x=>console.log(x)) //=>4
期约对象的链式操作有三个方法:then()、catch()、finally()。这三个方法都接收一个函数作为参数,表示下一步的操作。
当期约对象的状态变为resolved时,就可以调用then(),例如:
const 期约 = Promise.resolve(1)
期约.then(x=>x+1)
.then(x=>x*2)
.then(x=>console.log(x)) //=>4
当期约对象的状态变为rejected时,就可以调用catch(),不过,一次正常的catch()之后,状态就会变为resolved,例如:
const 期约= Promise.reject(1)
期约.catch(x=>x+1)
.then(x=>x*2)
.then(x=>console.log(x))
当期约对象无论是什么状态,都可以调用finally():
const 期约= Promise.reject(1)
期约.finally(()=>console.log('期约链开始'))
.catch(x=>x+1)
.then(x=>x*2)
.then(x=>console.log(x))
.finally(()=>console.log('期约链停止'))
期约链是一个串行操作,像流水线一样,每道工序可能包含:自己的操作、流转的数据,如果不显式或隐式地使用return为下一个操作提供数据,那么该步操作会直接将上一步的数据流转到下一步。也就是说,每一步肯定有一个流转数据,就看你用不用、处不处理,每一步也肯定会输出流转数据到下一步。流转的数据就是每一步都回调函数的第一个参数,如下示例:
const 期约= Promise.resolve(1)
期约.then((x)=>console.log('期约链开始'))
.then(x=>{
x=x+1
return 3
})
.then(x=>x*2)
.then((x)=>console.log(`现在流转的数据是${x}`))
在上面这个例子中,由Promise.resolve()产生期约链条的流转数据1,每一步的操作其实就是一个箭头函数,箭头函数的第一个参数是流转的数据,函数体是对流转数据的处理或其它行为,第一个then()没有对流转数据进行处理,所以直接往下传递。第二个then()对流转数据进行处理,但是显式地return 3,此时流转数据就是3。第三个then()对流转数据乘以2,注意,根据箭头函数的规则,这一行其实隐式地return了6。最后一个then()是获取流转数据并输出。
由上面的几个例子还可以看出,期约的链式操作是打平的,解决了以往的回调函数层层嵌套的问题,写法上更直观,理解起来也更直接。
顶层await
async定义的函数是同步求值的,await关键字是真正的异步。
以往,await只能写在async函数里面,ES2022新增了顶层await特性,允许await在函数外面书写。
要在node环境中测试最新(ES2022)的顶层await特性,需要将后缀名改为mjs。
来看一个例子:
// await.mjs
const data=1
const result1=await data+1
const result2=await result1*2
const result=await result2
console.log(await result) //=>4
迭代器
什么是可迭代对象
对于某种数据类型,如果它的元素可以按照确定的顺序进行有限的读取,那么我们认为这个数据类型是可迭代对象。所以,可迭代对象有两大关键特征:
- 元素的数量是有限的
- 元素的顺序是确定的
因此,数组、Map、Set都是可迭代类型。要特别注意两种类型:string是可迭代对象,里面的字符是它的元素;而Object类型不是可迭代对象,因为Object的元素的顺序是不确定的。
从可迭代对象创建迭代器
所有的可迭代对象都有一个Symbol.iterator方法,使用该方法可以返回一个迭代器,例如:
const arr = [1,2,3]
const iter = arr[Symbol.iterator]()
返回的迭代器有一个next()方法,不断得弹出元素值,done属性为false表示还有值可以被弹出,直到done属性变为true表示至此所有元素已经"耗尽":
iter.next() //=> {value:1, done:false}
iter.next() // => {value:2, done:false}
iter.next() // => {value:3, done:true},元素耗尽
iter.next() //=> {value:undefined ,done:true},后面的值都将是undefined
生成器
生成器的作用是生成一个自定义的可迭代对象,通过yield不断生成元素,通过return生成终止。生成器是一个函数,与普通函数的区别是在函数名称前面加上了一个星号。
function * generator() {
yield 1
yield 2
return 3
}
const iter = generator()
iter.next() //=> {value:1, done:false}
iter.next() // => {value:2, done:false}
iter.next() // => {value:3, done:true},元素耗尽
iter.next() //=> {value:undefined ,done:true},后面的值都将是undefined
可迭代对象的forEach()方法
forEach()是大多数可迭代对象都具有的方法,forEach()方法接收一个回调函数作为参数,该函数接收三个参数:
- 可迭代对象的元素
- 该元素的索引位置
- 可迭代对象本身,可以根据当前可迭代对象的类型定义一个意义明显的参数名称。
const map = new Map().set('a',1).set('b',2).set('c',3)
map.forEach((element,index,map)=>{
console.log(`键${index}的值是${element}`)
})
// 输出:
// 键a的值是1
// 键b的值是2
// 键c的值是3
代理
ES6标准新增的代理与反射为开发者提供了拦截并向基本操作嵌入额外行为的能力。比如我们在读取或设置一个Object的属性的值的时候,有时候需要加入额外操作,以提供响应式的能力。大受欢迎的Vue3.x就大量运用了这种特性,成为最受欢迎的响应式框架。
代理就是目标对象的抽象,它拦截了外界对目标对象的直接访问,从而有效的保护了目标对象。拦截操作全部都定义在捕获器对象中。
虽然ES5时代的Object.defineProperty()方法也能实现类似的目标,但是它对于目标对象是破坏性的操作,而我们今天要说的代理是一种非破坏性的操作。
创建空代理
空代理就是什么操作也不拦截,这个时候的代理对象只是起到一个“传话筒”的作用。从代码表现来看,此时捕获器是一个空对象。
const target = {a:1, b:2}
const handler = {} // 拦截操作是空的
const proxy = new Proxy(target,handler)
proxy.a // => 1 // 如实返回
proxy.b // =>2
get()捕获器、set()捕获器和反射API
实际上,使用代理最常添见的就是在捕获器之中加get()和set()方法。get()方法接收三个参数,分别是:目标对象,正在读取的目标对象的属性,代理对象。set()方法接收四个参数,分别是:目标对象,正在设置的目标对象的属性,设置的新值,代理对象。
同时,利用Reflect对象的API,可以快速将对代理的操作传递到目标对象上。
const obj = {a:1, b:2}
const handler = {
get(target,property,receiver){
console.log(`你正在读取${property}属性`)
},
set(target,property,value,receiver){
console.log(`你将${property}属性的值改成了${value}`)
Reflect.set(...arguments) // 将修改操作传递到目标对象上
},
}
const proxy = new Proxy(obj,handler)
console.log(proxy.a) // =>'你正在读取a属性'
proxy.b = 3 // => '你将b属性的值改成了3'
console.log(obj.b) //=>3,目标对象的属性值也跟着改了
DOM
Node类型
Node类型表示一个HTML文件中所有的节点。
HTML文件中所有实例都是节点,包括document节点、html元素节点、text节点。所以html元素类型、text节点类型都是是Node类型的子类:
下面这个层级结构的所有对象都是Node类型的实例,括号里面标识的是具体的类型:
document (Node)
html (HTMLElement)
head (HTMLElement)
title (HTMLElement)
text (textNode)
body (HTMLELement)
div (HTMLELement)
h1 (HTMLELement)
text (textNode)
p (HTMLELement)
text(textNode)
一个Node实例的操作主要分为两个大类:
- 获取父子兄弟节点
- 对子节点的节点的增、删、插入
获取父子兄弟节点,有如下操作,其中,someNode表示一个节点实例:
- someNode.childNodes,返回someNode的所有子节点,这是一个类数组对象。
- someNode.firstChild,返回第一个子节点实例。
- someNode.lastChild,返回最后一个子节点实例。
- someNode.parentNode,返回父级节点实例。
- someNode.nextSilbing,返回someNode的下一个兄弟节点。没有则返回null。
- someNode.previousSilbing,返回上一个兄弟节点。没有则返回null
上面提到的类数组对象的意思是,虽说返回的对象不是Array的实例,但是我们可以把这个节点列表当成一个数组来使用,例如如下用到的属性和方法:
- length属性,表示节点列表的数量。
- 返回第n个节点实例,由于节点列表是个类数组对象,因此直接使用中括号访问即可,例如someNode.childNodes[0]。
当然,要将这个节点列表转化为真正的数组对象,可以使用:
const nodeArr=Array.from(someNode.childNodes)
document对象
document对象主要用来获取节点实例。
一些特殊的节点实例的获取方法如下:
- document.body,取得节点实例
- document.head,取得节点实例
- document.title,取得文档的标题,也就是<head>下的<title>节点中的文本。
- document.URL,取得页面的URL。
- document.domain,取得页面的域名。
- document.images,取得页面上所有<img>元素,这是一个类数组对象。
- document.links,取得文档中所有带href属性的<a>节点,这是一个类数组对象。
要根据元素的特征获取节点,有如下操作:
- document.getElementById(‘IdName’),根据元素的id属性获取节点实例。
- document.getElementsByClassName(‘ClassName’),根据元素的class名称获取节点实例,注意是"Elements",后缀名加了s,因为允许多个节点共用一个类,返回一个类数组对象。如果要同时匹配多个类名,类名之间用空格隔开即可。
- document.getElementsByTagName(‘TagName’),根据元素的标签名返回一个类数组对象,注意标签名要大写,例如“DIV”,而不能是“div”。
- document.querySelector(‘Selector’),该方法接收一个选择器,返回第一个匹配的节点实例,没有找到则返回null。
- document.querySelectorAll(‘Selector’),该方法返回返回匹配的所有节点列表,是一个类数组对象。
Node.js
Node.js 基础
安装Node.js
打开浏览器,进入Node.js官网(https://nodejs.org/en/),可以选择LTS版本及Current版本,根据需要下载安装即可。默认会一起安装npm包管理器。
Node.js REPL
安装Node.js之后,应用程序中会多出Node.js图标,点击即进入Node.js交互式运行环境(REPL),在这里可以运行一些简单的JavaScript代码。
在命令行中直接输入JavaScript单行代码,然后回车,即可立即得出结果,如下图所示:
Node.js REPL的子命令和快捷键的作用如下:
子命令 | 作用 |
---|---|
.help | 显示帮助 |
.exit | 退出Node.js REPL |
.editor | 进入编辑器模式 |
.load | 载入外部的JS文件到REPL |
.save | 保存当前REPL所有的历史命令到文件 |
Ctrl+C | 取消当前的输入 |
Ctrl+D | 退出REPL |
Node.js 运行JavaScript代码
如果需要运行多行代码,建议在专门的编辑器(推荐Visual Studio Code)中编写JavaScript,然后在命令行中切换到该文件所在的目录,使用node 文件名.js
命令运行JavaScript文件。
引入Node.js内置模块
Node.js提供了大量的内置模块供开发者非常方便的与本地文件系统交互,要引入内置模块,有两种方式。
第一种方式是使用require()函数,如下:
const fs = require('fs')
第二种方式是使用import 关键字,这种方式是为了更好地兼容ES6的模块化,如下所示:
import fs from 'node:fs'
文件路径模块 —— Path
path模块用于处理文件系统的路径,这是一个很有用的模块。
使用path.join()合并路径
path.join()可以将传入的各个参数拼接成一个完整的路径。例如:
const path = require('path')
const pathStr = path.join('D:\\','Program Files')
console.log(pathStr) // D:\Program Files
注意,Windows系统路径里面的反斜杠都需要进行转义。
上面的例子似乎可以直接使用字符串的拼接,但是path.join()的强大之处在于可以处理相对路径、连续路径,如下所示:
const path = require('path')
const pathStr = path.join('D:\\','a','b\\c','..','.','1.txt')
console.log(pathStr) // D:\a\b\1.txt
使用path.basename()提取文件名
可以使用path.basename()提取一段路径中的文件名,例如:
const path = require('path')
const pathStr = path.join('D:\\','a','b\\c','..','.','1.txt')
console.log(pathStr) // D:\a\b\1.txt
const basename = path.basename(pathStr)
console.log(basename) // 1.txt
注意返回的结果是包含后缀名的。
使用path.dirname()提取目录名
可以使用path.dirname()提取目录名,例如:
const path = require('path')
const pathStr = path.join('D:\\','a','b\\c','..','.','1.txt')
console.log(pathStr) // D:\a\b\1.txt
const basename = path.dirname(pathStr)
console.log(basename) // D:\a\b
使用path.extname()提取扩展名
可以使用path.extname()提取扩展名,例如:
const path = require('path')
const pathStr = path.join('D:\\','a','b\\c','..','.','1.txt')
console.log(pathStr) // D:\a\b\1.txt
const basename = path.extname(pathStr)
console.log(basename) // .txt
注意,返回的不是txt,而是 .txt 。
文件内容的读写 —— fs模块
使用fs.readFileSync()读取文件内容
可以使用fs.readFileSync()读取文件内容,例如有下面一个文件1.txt,内容为:
Hello,World!
Hello,JavaScript!
Hello,Node.js!
const fs = require('fs')
const result = fs.readFileSync('1.txt','utf8')
console.log(result) // 原样输出1.txt的内容
一般情况下,我们读取的是文本类型的文件,所以一般使用utf8编码。
另外,有些时候,我们需要按行读取,返回一个由每行的内容组成的数组:
const fs = require('fs')
const content = fs.readFileSync('1.txt','utf8')
const result = content.split('\r\n')
console.log(result) // [ 'Hello,World!', 'Hello,JavaScript!', 'Hello,Node.js!' ]
console.log(result[0]) // Hello,World!
由于Windows和Linux在换行时对字符编码的差异,因此在Windows下使用\r\n,而在Linux下使用\n,这一点要注意。
使用fs.writeFileSync()写入文件内容
可以使用fs.writeFileSync()写入文件内容,例如:
const fs = require('fs')
const content = 'Hello,World!\r\nHello,JavaScript!\r\nHello,Node.js!'
fs.writeFileSync('2.txt',content,'utf8')
如果当前目录下存在2.txt,就会先清空2.txt,然后写入新内容。如果不存在2.txt,就会先新建2.txt,然后写入内容。
使用fs.appendFileSync()追加文件内容
可以使用fs.appendFileSync()追加文件内容,例如:
const fs = require('fs')
fs.appendFileSync('2.txt','\r\nHello','utf8')
同样地,如果文件存在就追加内容,如果文件不存在就先新建文件再写入内容。
目录的读写 —— fs模块
使用fs.readdirSync()读取目录
可以使用fs.readdirSync()读取目录,例如:
const fs = require('fs')
const files = fs.readdirSync(__dirname)
console.log(files)
这会返回所运行的脚本文件所在的目录下的由文件名组成的数组,这里的__diraname表示脚本文件所在的目录。
使用fs.statSync()分析文件或目录的属性
可以使用fs.statSync()分析文件或目录的属性,例如:
const fs = require('fs')
const stat1 = fs.statSync(__dirname)
const stat2 = fs.statSync('1.txt')
console.log(stat1,'\r\n',stat2)
这会分别输出当前目录、当前目录下1.txt的属性。例如:uid、gid、文件大小、创建时间、更改时间等。
os模块
Node.js的os模块提供了对操作系统相关信息的访问能力,有两种方式引入该模块:
// 方式一:
const os = require('os')
// 方式二:
import os from 'node:os'
该模块相关的方法和属性如下表所示:
方法&属性 | 作用 |
---|---|
os.tmpdir() | 返回操作系统默认的临时文件夹 |
os.hostname() | 返回操作系统的主机名 |
os.arch() | 返回操作系统的CPU架构 |
os.uptime() | 返回操作系统的运行时间,以秒为单位 |
os.cpus() | 返回操作系统的CPU相关的信息 |
os.EOL | 返回操作系统的行尾符,Windows是\r\n,Linux和MacOS是\n |
process 模块
process模块是Node.js的全局模块,无需引入,即可访问,该模块相关的方法或属性如下表所示:
方法&属性 | 作用 |
---|---|
process.version | 当前Node.js的版本号 |
process.env | 操作系统的环境变量env |
proces.argv | 返回一个数组,第一个元素为node二进制文件的绝对路径,第二个元素为所运行的.js文件的绝对路径,之后的元素为传入命令行的各个参数 |
process.cwd() | 当前工作路径 |
process.chdir(dir) | 切换当前工作路径 |
process.arch | 当前操作系统的架构,如x64 |
JavaScript 工具链
Vite
第一个Vite Vue项目
npm init vite ProjectName -- --template vue
使用Vite基本上就是使用这一行命令,所以只要记住这行命令即可,然后运行:
cd ProjectName
npm i
npx vite
配置文件
Vite 会自动解析项目根目录下名为 vite.config.js 的文件。
默认内容是这样的:
export default{
// 配置内容
}
一些有用的配置项
server.host:127.0.0.1 占用的IP地址,命令行用--host指定
server.port:3000占用的端口,命令行用--port指定
server.https:false 启用HTTPS
server.open:"index.html"自动在浏览器打开
zx.js
zx.js简介
要编写一个系统脚本,通常使用Shell。不过,如果你更喜欢JavaScript的语法,那么可以使用谷歌开源的zx.js,这个框架允许你使用JavaScript语法编写系统脚本,同时可以调用系统的命令行。
注意,下文的一切都在Linux系统下运行,推荐使用WSL。
安装
使用npm安装zx.js工具,推荐安装到本地:
npm i zx
使用
zx.js规定脚本必须写在.mjs文件中。然后就可以开始编写脚本了,先编写一个最简单的helloworld.mjs,内容如下:
console.log("hello,world")
然后,使用 npx zx helloworld.js ,会看到控制台会输出"hello,world"。
这样看,似乎zx.js与node.js的功能差不多。zx.js的功能体现在主要体现在两点:
-
$
<command>
:表示一行命令。 -
cd()、fetch()等函数:可以在文件中切换工作目录、请求HTTP等。
第一个示例
先通过一个例子感受下使用zx.js编写的脚本的格式:
#! /usr/bin/env node
import { $ } from "zx";
$.verbose = false
const output = (await $`ls`)
console.log(output)
使用$<command>
执行Linux命令
以往,要批量执行多条Linux命令,只能在.sh脚本中写。现在可以使用$<command>
。例如文件ls.mjs,内容如下:
await $`ls`
这个ls.mjs脚本等价于在运行ls命令,运行npx zx ./ls.mjs将输出当前目录下的文件列表。
反引号中可以是任意复杂的Linux命令,例如
await $`cat package.json | grep name`
反引号中可以使用变量,遵循JavaScript模板字符串语法,例如:
const dirName = 'test'
await $`mkdir ${dirname}`
这会在当前目录下新建一个子目录。
zx.js可以将JavaScript语法与Linux命令相结合,例如:
const numbers = [1,2,3,4,5]
async function func(x){
await $`touch file${x}.txt`
}
numbers.forEach(func)
运将会在当前目录下新建file1.txt、file2.txt、file3.txt、file4.txt、file5.txt五个文件。
zx.js 使用示例
如下代码会在当前目录新建两个文件和一个目录:
// example1.mjs
import {$} from 'zx'
$.verbose = false
await $`touch 1.txt 2.txt && mkdir test`
如下代码会将写入和追加文本内容到 1.txt 中:
// example2.mjs
import {$} from 'zx'
$.verbose = false
await $`echo "hello,zx.js" > 1.txt`
await $`echo "1\n2\n3\n4\n5" >> 1.txt`
如下代码会读取 1.txt 中的内容并输出:
// example3.mjs
import {$} from 'zx'
$.verbose = false
const output = (await $`cat 1.txt`).stdout
console.log(output)
如下代码读取 1.txt 的文件,然后重定向到 2.txt 中:
// example4.mjs
import {$} from 'zx'
$.verbose = false
await $`cat 1.txt > 2.txt`
函数
zx.js中常用的函数主要有两种:cd()、fetch()。
cd()用于切换工作目录,等价于Linux的cd命令,例如,在Linux环境下:
cd('/tmp')
工作目录将切换到/tmp。
npm
npm的安装
npm默认随着node一起安装,无需单独安装。运行npm -v
测试其版本。
更换镜像源
运行npm config set registry https://registry.npmmirror.com/
将镜像源更换为淘宝npm镜像源。运行npm config get registry
测试镜像源是否更换成功,如果输出https://registry.npmmirror.com/则表示镜像源更换成功。
使用 npm 安装、卸载、查看和更新第三方包
运行 npm install <package>
即可在./node_modules(本地)安装第三包,install可以使用i简写,即 npm i <package>
。
要卸载第三方包,运行 npm uninstall <package
即可。
使用 npm ls
会列出本地已安装的所有包,使用 npm ls <package>
会显示这个包的详细信息。
要更新第三方包,运行npm update 。同时可以使用npm update npm 更新npm自身的版本。
使用npm run运行脚本
通常,在项目根目录下,有一个package.json文件,文件中有 ‘scripts’ 字段,每一项包括键和值,可以使用 npm run <键>
运行脚本。例如:
"scripts":{
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
}
运行 npm run build
等价于运行 npx vite build
。
自定义脚本
在scripts之下,添加一个json条目,条目的键名为脚本名称,键值为要运行的命令,该名称相当于要在命令行运行的命令。例如:
"scripts":{
"dev": "vite",
"build": "vite build",
"preview": "vite preview",
"test":"echo hello,npm!"
}
此时,运行npm run test ,命令行会输出 “hello,npm”。
使用npx运行二进制命令
有些比较大的npm包会自带二进制命令,如果是使用npm install <package>
本地安装的,二进制命令会被放在./node_modules/.bin目录下。无需将该目录下的命令添加到系统PATH环境变量,只要在项目根目录或者其下的任意子目录,使用npx <二进制命令>
即可运行。
npm全局安装的根目录
如果使用npm install -g <package>全局安装
,默认情况下,会安装在C:\Users\<userName>\appData\Roaming\npm
目录下 。可以使用npm root -g
查看全局安装目录。不过,有了npx之后,建议都安装在本地。
Electron
安装
npm i electron
main.js
main.js是应用程序的入口文件。内容如下:
const { app, BrowserWindow } = require('electron')
const mainWindow = null
app.on('ready',()=>{
mainWindow = new BrowserWindow({
width:500,
height:500,
webPreferences:{
nodeIntegration:true //设置为true就可以在这个渲染进程中调用Node.js
}
});
mainWindow.loadFile('index.html'); // 加载本地文件
// mainWindow.loadURL('https://zhuiyi.ai/'); // 加载远程文件
mainWindow.webContents.openDevTools({ mode: 'bottom' }); // 控制台开关
mainWindow.on('close',()=>{
// 在窗口要关闭的时候触发
e.preventDefault(); // 避免进程意外关闭导致进程销毁
})
mainWindow.on('closed',()=>{
// 当窗口已经关闭的时候触发
})
})
index.html
main.js中提到的index.html就是第一个页面。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Hello World!</title>
</head>
<body>
<h1>Hello World!</h1>
</body>
</html>
启动应用
npx electron .
Vue.js
开启一个Vue项目
开启一个Vue项目最简单的方式是使用Vite。在安装了Node.js、npm(默认随着Node.js安装)之后,在当前目录的命令行中输入如下命令:
npm init vite my-project -- --template vue
注意这里中间的双短横线不能省略。然后依次运行如下命令:
cd my-project
npm i
npx vite
这样就启动了一个本地静态文件服务器,命令行的输出内容中会包含一个网址,一般为http:localhost:3000,在浏览器中打开这个网址,即可看到默认的Vue3+Vite页面。
实际上,对于Vite的使用,只需要记住上面的那一行命令即可。接下来,我们总是在这个项目下, 使用Vue单文件(*.vue)的形式学习使用Vue.js。
Vite项目的文件结构
使用Vite3创建的Vue项目中,index.html在根目录下,作为项目的入口页面,其最重要的内容有两个,第一个是页面中唯一的div元素:
<div id="app">
</div>
第二个重要内容是<script>标签:
<script src="./src/main.js"> </script>
main.js的内容如下:
import { createApp } from 'vue'
import App from './App.vue'
const app = createApp(App)
app.mount('#app')
第一行代码从vue包中引入了creatApp()函数。第三行代码导入App.vue的内容,作为根组件的配置信息。第五行代码创建应用示例,第七行代码将该实例挂载到页面。
根组件App.vue是我们编写内容的起点,我们编写的自定义组件(*.vue文件)都要“汇聚”到根组件中。
根组件的主要内容如下:
<script setup>
import <OtherComponent> from './components/OtherComponent'
</script>
<template>
<OtherComponent />
</template>
<style>
</style>
其中<script setup>部分导入vue函数、导入其它组件、编写JavaScript逻辑,<template>部分编写页面元素、使用其它组件作为页面元素,<style>部分定义样式。
对象类型的响应式对象
一般使用ref()创建原始值的响应式对象,而使用reactive()创建对象的响应式对象。例如:
<script setup>
import {reactive} from 'vue'
const data = reactive({msg:'Hello,Vue'})
</script>
<template>
<h1>{{data.msg}}</h1>
</template>
对象类型的响应式对象是深层次的,这意味着嵌套的对象、数组、Map、Set也是响应式的,例如:
<script setup>
import {reactive} from 'vue'
const data = reactive({
arr:[1,2,'Hello'],
map:new Map([ ['a',1],['b',2] ]),
set:new Set([1,2,'Vue'])
})
</script>
<template>
<h1>{{data.arr}}</h1>
<h1>{{data.map}}</h1>
<h1>{{data.set}}</h1>
</template>
列表渲染
v-for指令用来迭代渲染数组的内容,例如:
<script setup>
import { ref } from 'vue'
const arr = [1,2,3]
</script>
<template>
<li v-for='item in arr'>
{{item}}
</li>
</template>
v-for的内容中,一个参数表示数组中的元素,两个参数分别表示数组元素、索引,例如:
<script setup>
import { ref } from 'vue'
const arr = [1,2,3]
</script>
<template>
<li v-for='(item,index) in arr'>
arr[{{index}}]:{{item}}
</li>
</template>
v-for还可以遍历对象,例如:
<script setup>
import { ref } from 'vue'
const obj = {a:1, b:2, c:3}
</script>
<template>
<li v-for='item in obj'>
{{item}}
</li>
</template>
v-for可以接收两个参数,分别表示属性值、属性名,例如:
<script setup>
import { ref } from 'vue'
const obj = {a:1, b:2, c:3}
</script>
<template>
<li v-for='(value,key) in obj'>
{{key}}:{{value}}
</li>
</template>
注意这里的顺序,不是(key,value),而是(value,key)。
v-for也可以和模板搭配在一起使用,用来包含多个元素,例如:
<script setup>
import { ref } from 'vue'
const obj = {a:1, b:2, c:3}
</script>
<template>
<template v-for='(value,key) in obj'>
<h1>{{key}}:{{value}}</h1>
</template>
</template>
这里的<template>与前面不同的是,template是一个空标签,只是作为渲染列表的容器,而前面的内容中,v-for所在的元素本身也会进入渲染列表。
条件渲染
v-if 指令用来条件性地渲染一块内容,当v-if指令的值为true时才会渲染该元素,例如:
<script setup>
import { ref } from 'vue'
const show = true
</script>
<template>
<h1 v-if='show'>Hello,World!</h1>
</template>
可以加上v-else用于v-if指令值为false时渲染的内容,例如:
<script setup>
import { ref } from 'vue'
const show = false
</script>
<template>
<h1 v-if='show'>Hello,World!</h1>
<h1 v-else>Hello,Vue!</h1>
</template>
注意,v-else是不能接等于号的,v-if和v-else是互斥的,始终只有一个元素被渲染出来。
还可以加上v-else-if用于多层嵌套的条件,例如一个简单的打分示例:
<script setup>
import { ref } from 'vue'
const grade = 72
</script>
<template>
<h1 v-if='grade>=80'>优秀😊</h1>
<h1 v-else-if='grade>=60'>良好😐</h1>
<h1 v-else>不及格😥</h1>
</template>
另外一个条件渲染的指令是v-show。v-show与v-if的区别在于,v-show只是隐藏,但是已经渲染出来了,也就是说,存在一个占位符。例如:
<script setup>
import { ref } from 'vue'
const show = false
</script>
<template>
<h1 v-show='show'>此条隐藏</h1>
<h1 v-show='!show'>此条显示</h1>
</template>
响应式更新HTML元素的值
在没有使用Vue的时候,HTML元素的属性值是固定的,例如:
<a href="https://www.baidu.com"></a>
要将href属性的值进行响应式更新,只需要在其前面加上一个冒号,这样,Vue就会把后面的引号中的内容当做变量名,如下:
<script setup>
import { ref } from 'vue'
const url1 = ref('https://www.baidu.com')
const url2 = ref('https://www.mi.com')
</script>
<template>
<a :href="url1">网址</a>
</template>
此时,就可以通过JavaScript代码动态设置该链接的href了。
这里的冒号是v-bind:的语法糖,当你需要将元素的属性值变量化的时候,在该属性名前面加上v-bind:或者:,vue就不再把该属性的值当做普通内容,而是当做一个变量名。
响应式更新style样式
内联的style样式是HTML元素的常用属性,例如:
<script setup>
import { ref } from 'vue'
</script>
<template>
<div style="width:100px;height:100px;background:red">
</div>
</template>
这会在页面渲染出一个红色的框,长、宽为100px。
要动态更新内联样式,首先把style的内容转成Vue风格的代码,如下:
<script setup>
import { ref } from 'vue'
</script>
<template>
<div :style="{ width:'100px',height:'100px',background:'red' }">
</div>
</template>
相比于原先的代码,除了在style前面加上了冒号,还在首尾的双引号之间加上了花括号,以此让Vue当做一个对象处理,所以style里面的属性的值都要写成或者被解析成字符串。另外需要注意的是,要将分号替换成逗号。这里没什么难得,只是有很多细节需要注意。
然后,将需要响应式更新的属性值替换成变量即可:
<script setup>
import { ref } from 'vue'
const width = ref(200)
const height = ref(200)
const color = ref('hsl(120,50%,50%,0.5)')
</script>
<template>
<div :style="{ width:width+'px', height:height+'px', background:color }">
</div>
</template>
这里需要留意JavaScript的加操作符:数值类型和字符串类型相加时,数值类型会首先隐式转换为字符串类型。
响应式更新class属性值
除了响应式更新内联的style样式,也可以响应式更新class属性值。来看这个例子:
<script setup>
import { ref } from 'vue'
</script>
<template>
<div class="red">
</div>
</template>
<style scope>
div{
width:100px;
height:100px;
}
.red{
background:red
}
.green{
background:green
}
</style>
在这个例子中,class的属性值是固定的,不能通过变量修改,使用响应式系统,class的属性值就是一个变量,如下:
<script setup>
import { ref } from 'vue'
const style = ref('green')
</script>
<template>
<div :class="style">
</div>
</template>
<style scope>
div{
width:100px;
height:100px;
}
.red{
background:red
}
.green{
background:green
}
</style>
总而言之,只需要在Vue中给元素的属性名前面加上冒号,再给定变量,就可以响应式更新HTML元素的属性值了。
原始值和其它数据类型的响应式对象
ref()第一个示例
使用ref()可以很方便的创建双向响应式数据。先看一个例子:
<script setup>
import {ref} from 'vue'
const data = ref('Hello,Vue')
</script>
<template>
<h1>{{data}}</h1>
<input v-model="data" />
</template>
这样,h1标题的内容和input输入框中的内容总是一样的,当你通过输入改变input的内容是,h1的内容也会跟着改变。
ref()的解包
当ref()返回的对象在<template>中作为顶层属性被访问时,它们会自动解包,所以不要使用.value,例如:
<script setup>
import {ref} from 'vue'
const data = ref('Hello,Vue')
</script>
<template>
<h1>{{data.split(",")}}</h1>
<input v-model="data" />
</template>
这里的<h1>中,data是作为顶层属性被使用,因此不能使用.value,否则会报错。
再比如:
<script setup>
import {ref} from 'vue'
const data = ref(1)
</script>
<template>
<h1>{{data+1}}</h1>
</template>
这里也不能使用.value。
看下面的例子:
<script setup>
import {ref} from 'vue'
const data = ref(1)
const obj = {foo:data}
</script>
<template>
<h1>{{obj.foo+1}}</h1> <!--[object Object]1-->
<h1>{{ obj.foo.value+1 }}</h1> <!--2-->
</template>
在这个例子中,obj.foo 求值为data,但此时data不是作为顶层属性被访问,data此时是一个对象:{value:1},对象与数字相加遵循JavaScript的语法规则。因此如果目的是为了使其数值相加,则需要使用.value手动解包。
数组类型的响应式对象
要创建数组类型的响应式对象,使用ref(),例如:
<script setup>
import {ref} from 'vue'
const data = ref([1,2,3])
</script>
<template>
<h1>{{data.at(-1)}}</h1>
<input v-model="data" />
</template>
当在输入框中添加新元素时,h1始终显示最后一个元素值。
组件传值
在实际应用中,有时候我们要多次复用同一个组件,只是修改组件的部分参数,以实现模块的解耦和定制化,这就涉及父组件、子组件,以及父组件向子组件传值。
首先编写一个子组件Son.vue,如下:
<script setup>
import { ref } from 'vue'
const props = defineProps(['msg'])
</script>
<template>
<h1>{{msg}}</h1>
</template>
在这个子组件Son.vue中,定义了一个名称为msg的props,msg将通过父组件接收明确的值,同一目录下的父组件App.vue内容如下:
<script setup>
import { ref } from 'vue'
import Son from './Son.vue'
</script>
<template>
<Son msg="Hello,Vue!"></Son>
</template>
msg的值已经传递到子组件了,这种方式是静态传递。还可以将元素属性msg变量化:
<script setup>
import { ref } from 'vue'
import Son from './Son.vue'
const data = "Hello,Vue!"
</script>
<template>
<Son :msg="data"></Son>
</template>
这时,由于在msg前面加了冒号,因此data不是字面值,而是被当做一个变量标识符。
也可以定义多个props,子组件内容更新如下:
<script setup>
import { ref } from 'vue'
const props = defineProps(['msg1','msg2'])
</script>
<template>
<h1>{{msg1}}</h1>
<h1>{{msg2}}</h1>
</template>
父组件内容更新如下:
<script setup>
import { ref } from 'vue'
import Son from './Son.vue'
const data1 = "Hello,World!"
const data2 = "Hello,Vue!"
</script>
<template>
<Son :msg1="data1" :msg2="data2"></Son>
</template>
CSS
选择符
元素选择符
使用元素的标签名,例如:h2、p、div、img
id选择符
使用#加上元素的id。例如#first表示id为first的元素。id在HTML文档中唯一,两个元素不能使用同一个id。
类选择符
例如.success表示class为success的元素,p.success表示class为success的p元素
群组选择符
使用逗号将各选择符分隔,表示匹配所有列出的元素。例如:h1,h2,h3,p,#myDiv,.myClass
通用选择符
使用*
属性选择符
选择具有某个属性的元素,例如p[class]表示具有class属性的p元素,而不管class属性的值是什么。
也可以确定属性的值,例如*[class=“success”]表示class属性的值为success的p元素。
也可以模糊匹配属性的值,例如*[class*=‘success’]表示class属性值包含success子字符串的p元素。这样class属性值为success、success-info等元素都能匹配到。
有的时候,class属性值是一组词,表示该元素使用了多个样式,例如:
<p class="info red"></>
这时若要匹配info,则可以使用*[class~=“info”],表示匹配一组词中的一个,如果没有这个波浪号,那么就是精确匹配info了。
后代选择符
使用空格表示后一个符号是前一个的后代,例如#nav p匹配id为nav的元素中的p元素后代。这里的后代不仅包括子级元素,还包括子级的后代等元素。
子级选择符
有时候不需要匹配后代,而只需要匹配到子级元素,这时使用>符号。例如#nav>div表示在id为nav的元素的子级中,寻找div元素,但是不继续往下找。
紧邻同胞元素
紧邻同胞元素的意思是选择同级别的下一个元素,例如h1+p表示紧跟在h1元素后面的第一个p元素。这里最重要的是要注意两个关键词:紧跟、第一个,如下所示:
<!--h1+p匹配不到,因为中间插入了h2-->
<h1></h1>
<h2></h2>
<p></p>
<!--紧跟在h1后面的第一个p元素-->
<h1></h1>
<p></p>
<p></p> <!--匹配不到-->
<h2></h2>
选择后续同胞
选择后续同胞相对于紧邻同胞要宽松一些,就是在同级别的元素中往后找,找到第一个匹配到的就停止,并不需要是紧跟在后面。
伪类选择符
所有的伪类都是一个冒号(:)后面跟着一个单词。
如下CSS代码表示鼠标悬停时,未访问链接显示为蓝色,已访问链接显示为灰色:
a:link:hover {color:blue}
a:visited:hover {color:gray}
如下表示选择#nav元素的第一个子元素:
#nav > :first-child
对应的,如下表示选择#nav元素的第一个子元素:
#nav > :last-child
使用:nth-child(n)还可以选择任意第n个子元素,例如,如下表示选择#nav元素的第3个子元素:
#nav > :nth-child(3)
所以,如下两个选择符是等价的:
#nav > :first-child
#nav > :nth-child(1)
:nth-child()还可以表示等差数列,表示每隔几个元素,例如:
#nav > :nth-child(2n+1)
n从0开始取值,这段CSS代码表示#nav的第1、3、5、7…个子元素。
实际运用中通常需要对奇、偶子元素分别赋予不同的样式,例如单元格隔行着色,如下:
tr:nth-child(2n+1) {background:gray}
tr:nth-child(2n) {background:white}
上述代码表示表格的奇数行的背景色为灰色,偶数行的背景色为白色。
用户操作伪类
伪类 | 解释 |
---|---|
:link | 超链接 |
:visited | 已访问的,通常与:link搭配使用 |
:focus | 获得输入焦点的元素 |
:hover | 鼠标悬停的元素 |
:active | 用户激活的元素,例如当用户单击超链接按下鼠标按键的那段时间 |
装饰首字母
有时候需要将段落的首字母大写,这时可以使用::first-letter伪类,例如,如下表示把每个段落的首字母的颜色标红,且放大到2倍:
p::first-letter {
color:red;
font-size:200%;
}
装饰首行
如下表示将每个段落的首行的字体颜色标位绿色:
p:first-line {color:green;}
装饰内容
在元素前面插入两个井号(#)表示是二级标题:
h2::before{content:"##"}
字体属性
CSS属性 | 说明 | 取值示例 | 默认值 |
---|---|---|---|
font-family | 字体名称 | ||
font-weight | 字重 | normal(常规)、bold(加粗)、lighter、数值(如100) | normal |
font-style | 字体样式 | normal(常规)、italic(斜体)、oblique(倾斜) | normal |
font-size | 字体大小 | small、large、15px、1.5em、150% | medium |
line-height | 行高 |
文本属性
首行缩进
如下表示首行缩进2个字符:
p{text-indent:2em}
如果text-indent 为负值,则为悬挂缩进,不过这样会超出版面范围,所以需要先将段落整体往右移动,如下:
p{
padding-left:2em;
text-indent:-2em;
}
这样就实现了Word软件中的段落悬挂缩进功能。
文本对齐
text-align 表示文本对齐,取值及说明如下表所示:
取值 | 说明 |
---|---|
left | 左对齐 |
right | 右对齐 |
center | 居中对齐 |
justify | 两端对齐 |
行高
使用line-height控制段落中的行高,行高是值行之间的基线之间的距离。可取的值例如:
p.class1 {line-height:1.5em} // 行高为1.5倍字体大小
p.class1 {line-height:150%} // 行高为1.5倍字体大小
p.class2 {line-height:16px} // 行高为16px
单词间距
word-spacing 表示单词的间距,例如:
p{word-spacing:0.5em;} //每个单词有0.5个字符的间距
字符间距表示字符与字符之间的距离,例如:
p{letter-spacing:0.25em} // 字符与字符之间有0.25个字符的距离
字符装饰
text-decoration属性的作用是给文本添加一些装饰,例如下划线、删除线。
如果要给文本添加下划线,使用underline,例如:
p{text-decoration:underline;}
如果要给文本添加上划线,使用overline,例如:
p{text-decoration:overline;}
如果要给文本添加删除线,使用line-through,例如:
p{text-decoration:line-through}
文本阴影
默认情况下,文本没有阴影,如果要给文本添加阴影,使用text-shadow属性,该属性的值由多个值组成,第一个值表示阴影的颜色,第二个值表示横向偏移距离,第三个值表示纵向便宜距离,第四个值表示阴影的模糊半径,第三个和第四个值是可选的。
例如,如下CSS代码表示文本的阴影为红色,阴影向右侧偏移0.25em,向下方偏移5px,阴影的模糊半径为2px:
p{text-shadow: red 0.25em 5px 2px}
盒模型
- 高度和宽度:内容的高度和宽度
- 内边距:
- 外边距:
- 边框厚度:
高度和宽度
高度和宽度指的是内容区的高度和宽度。
内边距
内边距指的是内容和边框之间的距离,使用padding属性设置该距离。例如,如下表示将h2的文本内容与边框之间有2em的距离,并且背景色为灰色:
h2 {padding:2em; background-color:gray;}
padding属性的值得单位可以是em,表示相对于内容的字号大小设置的距离,也可以是px,表示绝对距离。
padding属性的值可以有1个值、2个值、3个值、4个值,如果值有1个,表示上下左右的距离一样。
如果值有2个,表示上下和左右分别取值,例如:
h2{padding:16px 10px} // 上下内边距为16px,左右内边距为10px
如果值有3个,分别表示上边距值、左右边距值、下边距值,例如
h2{padding:16px 10px 12px} // 上边距值为16px,左右边距为10px,下边距为12px
如果值有4个,分别表示上边距、右边距、下边距、左边距,是一个从上方开始逆时针旋转的方向,例如:
h2{padding:16px 10px 12px 14px} // 上边距16px,右边距10px,下边距12px,左边距14px
上面的方法是将四边的内边距一起写在padding里面,还可以单独设置某个边距的值,分别为:
属性 | 作用 |
---|---|
padding-top | 上内边距 |
padding-right | 右内边距 |
padding-bottom | 下内边距 |
padding-left | 左内边距 |
边框
边框有三个要素:宽度、样式、颜色。
边框的宽度
边框的宽度使用border-width属性,可取的值包括:
| 取值示例 | 说明 |
| -------- | ---------------- |
| thin | 细线 |
| medium | 常规宽度,默认值 |
| thick | 粗线 |
| 2px | 使用精确的数值 |
当然,border-width的取值也可以有1个、2个、3个、4个,例如:
p {border-width:2px} // 四边框等宽
p {border-width:2px 3px} // 上下边框2px,左右边框3px
p {border-width:2px 3px 4px} // 上边框2px,左右边框3px,下边框4px
p {border-width:2px 3px 4px 5px} // 上边框2px,右边框3px,下边框4px,左边框5px
除了使用border-width,每个边框的宽度也有单独的属性,包括:
属性 | 说明 |
---|---|
border-top-width | 上边框的宽度 |
border-right-width | 右边框的宽度 |
border-bottom-width | 下边框的宽度 |
border-left-width | 左边框的宽度 |
边框的样式
边框的样式使用border-style属性,可取的值如下表所示:
样式 | 说明 |
---|---|
none | 没有样式 |
hidden | 隐藏,与none一致 |
solid | 实线 |
dotted | 点划线 |
dashed | 虚线 |
double | 双线 |
groove | 沟槽 |
ridge | 山脊 |
inset | 内凹 |
outset | 外凸 |
与padding和margin一样,也可以针对上下左右四条边框分别设置,例如:
p{border-style: solid dotted dashed double}
上面的代码表示段落的上边框为实线、右边框为点划线、下边框为虚线、左边框为双线。
也可以单独设置某个边框的样式,包括:
属性 | 说明 |
---|---|
border-top-style | 上边框的样式 |
border-right-style | 右边框的样式 |
border-bottom-style | 下边框的样式 |
border-left-style | 左边框的样式 |
单边样式的属性值与border-style属性可取的值一致,不再赘述。
边框的颜色
边框的颜色使用border-color属性,属性值既可以是具名颜色,例如:red、grey,也可以是rgb()、hsl()、十六进制等。
边框的颜色的取值可以有1个、2个、3个、4个,例如:
p {border-color: red} // 四边框均为红色
p {border-color: red blue} // 上下边框为红色,左右边框为蓝色
p {border-color: red blue green} // 上边框为红色,左右边框为蓝色,下边框为绿色
p {border-color: red blue green grey} // 上边框为红色,右边框为蓝色,下边框为绿色,左边框为灰色
除了使用border-color一次性设置四个边框的颜色,也可以单独设置某个边框的颜色,如下表所示:
属性 | 说明 |
---|---|
border-top-color | 上边框的颜色 |
border-right-color | 右边框的颜色 |
border-bottom-color | 下边框的颜色 |
border-left-color | 左边框的颜色 |
整体设置
除了分别设置宽度、样式、颜色属性外,也可以直接对某个边框进行一次性设置,顺序依次为width、style、color,例如:
p{bottom-top:2px solid red} // 上边框的样式会实线,宽度为2px,颜色为红色
p{bottom-bottom: 3px dashed green} // 上边框的样式会虚线,宽度为3px,颜色为绿色
也可以直接使用border属性全部设置,例如:
p{border: 2px solid red} // 所有的边框宽度为2px,实线,红色
圆角边框
默认情况下,元素边框的四个角都是直角,可以使用border-radius属性定义圆角半径。例如:
p{border-radius:5px} // 四边的圆角均为5px
也可以取多个值,使得四个角的半径不同,例如:
p{border-radius: 5px 8px} // 左上角和右下角的半径为5px,右上角和左下角的半径为8px
p{border-radius: 5px 8px 6px} // 左上角的半径为5px,右上角和左下角的半径为8px,右下角的半径为6px
p{border-radius: 5px 6px 7px 8px} // 左上角的半径为5px,右上角的半径为6px,右下角的半径为7px,右下角的半径为8px
也可以单独对某一个角设置,包括:
属性 | 说明 |
---|---|
border-top-left-radius | 左上角的圆角半径 |
border-top-right-radius | 右上角的圆角半径 |
border-bottom-right-radius | 右下角的圆角半径 |
border-bottom-left-radius | 左下角的圆角半径 |
外边距
内边距是元素的内容与边框之间的距离,而外边距则指的是元素与外界之间的距离,包括与父元素、同级别元素。外边距使用margin属性设置,例如:
p {margin: 20px } // 段落四周与其它元素的距离为20px
上述代码设置四周的外边距等同,也可以使四周的外边距不同,例如:
p{margin: 10px 30px} // 上下外边距为10px,左右外边距为30px
p{margin: 10px 20px 30px} // 顶部外边距为10px,左右外边距为20px,底部外边距为30px
p{margin: 10px 20px 30px 40px} // 顶部外边距为10px,右侧外边距为20px,底部外边距为30px,左侧外边距为40px
当然,也可以对某个方向的外边距单独设置,包括:
属性 | 解释 |
---|---|
margin-top | 顶部外边距 |
margin-right | 右侧外边距 |
margin-bottom | 底部外边距 |
margin-left | 左侧外边距 |
渐变
线性渐变
线性渐变使用background-image属性,需要定义渐变的方向和至少两个色标。渐变的方向默认从上到下。
简单的线性渐变示例如下:
div{background-image: linear-gradient(red,blue)}
这行代码表示从上到下,由红色均匀渐变为蓝色。
可以使用角度值指定渐变的方向,例如;
div{background-image: linear-gradient(90deg,red,blue)}
这行代码表示从左到右,由红色渐变为蓝色。0deg表示从下到上,90deg表示方向顺时针旋转了90度。
还可以显示使用to关键字指定渐变的方向,例如:
div{background-image: linear-gradient(to right,red,blue)}
这行代码表示从左到右,由红色渐变为蓝色。
下面的代码表示从左上角到右下角,有红色渐变为蓝色:
div{background-image: linear-gradient(to bottom right,red,blue)}
角度和to关键字的对应关系如下表所示:
角度 | 关键字 | 解释 |
---|---|---|
0 | to top | 从下到上 |
45deg | to top right | 从左下角到右上角 |
90deg | to right | 从左到右 |
135deg | to bottom right | 从左上角到右下角 |
180deg | to bottom | 从上到下 |
225deg | to bottom left | 从右上角到左下角 |
-90deg | to left | 从右到左 |
以上渐变都是均匀的渐变,可以使用百分数表明色标的位置,以实现更精确的控制,例如:
div{background-image: line-gradient(to right,red,blue 15%, green 25%, yellow 60%, orange 80%, grey 100% )}
上面的代码表示从左到右的渐变,在0%处的颜色为红色,在15%处的颜色为蓝色,在25%处的颜色为绿色,在60%处的颜色为蓝色,在80%处的颜色为橘黄色,在100%处的颜色为灰色。
径向渐变
如下为一个简单的径向渐变:
div{background-image: radial-gradient(red, blue)}
这行代码表示从元素的中心开始均为向四周渐变,由红色过渡到蓝色。
也可以指定色标位置,例如:
div{background-image: radial-gradient(red 5%, yellow 15%, green 60%);}
定位
元素的位置使用position属性,属性值包括:
属性值 | 解释 |
---|---|
static | 默认,位于文档流中,随父元素流动 |
relative | 基于默认位置偏移一定的距离,但依然位于文档流中,随父元素流动,此时不会发生元素重叠 |
absolute | 元素从文档流中移除,相当于在文档流上新建了一个图层,此时可能会与文档流中的元素重叠,但依然随着窗口滚动 |
fixed | 元素始终位于视口中,不会随着窗口滚动而改变位置,例如许多网站很常见的顶部导航条 |
当position的属性值为relative、absolute、fixed时,可以设置偏移属性,包括:
属性 | 说明 |
---|---|
top | 相对于原先位置的顶部的偏移 |
right | 相对于原先位置的右侧的偏移 |
bottom | 相对于原先位置的底部的偏移 |
left | 相对于原先位置的左侧的偏移 |
式 |
单边样式的属性值与border-style属性可取的值一致,不再赘述。
边框的颜色
边框的颜色使用border-color属性,属性值既可以是具名颜色,例如:red、grey,也可以是rgb()、hsl()、十六进制等。
边框的颜色的取值可以有1个、2个、3个、4个,例如:
p {border-color: red} // 四边框均为红色
p {border-color: red blue} // 上下边框为红色,左右边框为蓝色
p {border-color: red blue green} // 上边框为红色,左右边框为蓝色,下边框为绿色
p {border-color: red blue green grey} // 上边框为红色,右边框为蓝色,下边框为绿色,左边框为灰色
除了使用border-color一次性设置四个边框的颜色,也可以单独设置某个边框的颜色,如下表所示:
属性 | 说明 |
---|---|
border-top-color | 上边框的颜色 |
border-right-color | 右边框的颜色 |
border-bottom-color | 下边框的颜色 |
border-left-color | 左边框的颜色 |
整体设置
除了分别设置宽度、样式、颜色属性外,也可以直接对某个边框进行一次性设置,顺序依次为width、style、color,例如:
p{bottom-top:2px solid red} // 上边框的样式会实线,宽度为2px,颜色为红色
p{bottom-bottom: 3px dashed green} // 上边框的样式会虚线,宽度为3px,颜色为绿色
也可以直接使用border属性全部设置,例如:
p{border: 2px solid red} // 所有的边框宽度为2px,实线,红色
圆角边框
默认情况下,元素边框的四个角都是直角,可以使用border-radius属性定义圆角半径。例如:
p{border-radius:5px} // 四边的圆角均为5px
也可以取多个值,使得四个角的半径不同,例如:
p{border-radius: 5px 8px} // 左上角和右下角的半径为5px,右上角和左下角的半径为8px
p{border-radius: 5px 8px 6px} // 左上角的半径为5px,右上角和左下角的半径为8px,右下角的半径为6px
p{border-radius: 5px 6px 7px 8px} // 左上角的半径为5px,右上角的半径为6px,右下角的半径为7px,右下角的半径为8px
也可以单独对某一个角设置,包括:
属性 | 说明 |
---|---|
border-top-left-radius | 左上角的圆角半径 |
border-top-right-radius | 右上角的圆角半径 |
border-bottom-right-radius | 右下角的圆角半径 |
border-bottom-left-radius | 左下角的圆角半径 |
外边距
内边距是元素的内容与边框之间的距离,而外边距则指的是元素与外界之间的距离,包括与父元素、同级别元素。外边距使用margin属性设置,例如:
p {margin: 20px } // 段落四周与其它元素的距离为20px
上述代码设置四周的外边距等同,也可以使四周的外边距不同,例如:
p{margin: 10px 30px} // 上下外边距为10px,左右外边距为30px
p{margin: 10px 20px 30px} // 顶部外边距为10px,左右外边距为20px,底部外边距为30px
p{margin: 10px 20px 30px 40px} // 顶部外边距为10px,右侧外边距为20px,底部外边距为30px,左侧外边距为40px
当然,也可以对某个方向的外边距单独设置,包括:
属性 | 解释 |
---|---|
margin-top | 顶部外边距 |
margin-right | 右侧外边距 |
margin-bottom | 底部外边距 |
margin-left | 左侧外边距 |
渐变
线性渐变
线性渐变使用background-image属性,需要定义渐变的方向和至少两个色标。渐变的方向默认从上到下。
简单的线性渐变示例如下:
div{background-image: linear-gradient(red,blue)}
这行代码表示从上到下,由红色均匀渐变为蓝色。
可以使用角度值指定渐变的方向,例如;
div{background-image: linear-gradient(90deg,red,blue)}
这行代码表示从左到右,由红色渐变为蓝色。0deg表示从下到上,90deg表示方向顺时针旋转了90度。
还可以显示使用to关键字指定渐变的方向,例如:
div{background-image: linear-gradient(to right,red,blue)}
这行代码表示从左到右,由红色渐变为蓝色。
下面的代码表示从左上角到右下角,有红色渐变为蓝色:
div{background-image: linear-gradient(to bottom right,red,blue)}
角度和to关键字的对应关系如下表所示:
角度 | 关键字 | 解释 |
---|---|---|
0 | to top | 从下到上 |
45deg | to top right | 从左下角到右上角 |
90deg | to right | 从左到右 |
135deg | to bottom right | 从左上角到右下角 |
180deg | to bottom | 从上到下 |
225deg | to bottom left | 从右上角到左下角 |
-90deg | to left | 从右到左 |
以上渐变都是均匀的渐变,可以使用百分数表明色标的位置,以实现更精确的控制,例如:
div{background-image: line-gradient(to right,red,blue 15%, green 25%, yellow 60%, orange 80%, grey 100% )}
上面的代码表示从左到右的渐变,在0%处的颜色为红色,在15%处的颜色为蓝色,在25%处的颜色为绿色,在60%处的颜色为蓝色,在80%处的颜色为橘黄色,在100%处的颜色为灰色。
径向渐变
如下为一个简单的径向渐变:
div{background-image: radial-gradient(red, blue)}
这行代码表示从元素的中心开始均为向四周渐变,由红色过渡到蓝色。
也可以指定色标位置,例如:
div{background-image: radial-gradient(red 5%, yellow 15%, green 60%);}
定位
元素的位置使用position属性,属性值包括:
属性值 | 解释 |
---|---|
static | 默认,位于文档流中,随父元素流动 |
relative | 基于默认位置偏移一定的距离,但依然位于文档流中,随父元素流动,此时不会发生元素重叠 |
absolute | 元素从文档流中移除,相当于在文档流上新建了一个图层,此时可能会与文档流中的元素重叠,但依然随着窗口滚动 |
fixed | 元素始终位于视口中,不会随着窗口滚动而改变位置,例如许多网站很常见的顶部导航条 |
当position的属性值为relative、absolute、fixed时,可以设置偏移属性,包括:
属性 | 说明 |
---|---|
top | 相对于原先位置的顶部的偏移 |
right | 相对于原先位置的右侧的偏移 |
bottom | 相对于原先位置的底部的偏移 |
left | 相对于原先位置的左侧的偏移 |