二.函数、对象与时间库
一.函数
1.函数的声明
在javascript
中,函数的声明与go
类似,其关键字为function
1
2
3
4
5
6
function hello() {
console.log("Hello")
};
hello()
//输出结果
hello
同样,在javascript
中也存在匿名函数,在javascript
和go
中,函数被视为一等公民,可以进行赋值给参数等多种操作。
与go
相同的地方是,javascript
中可以直接使用匿名函数,被称为立即执行函数。也可以将其给赋值给一个变量,然后再调用该变量。
1
2
3
4
5
6
7
8
9
10
const hello = function(){
console.log("Hello")
};
(function(){
console.log("World")
})();
hello()
//输出结果
hello
World
此处注意,一般而言要在函数末尾书写;
,否则可能出现语法错误。
除了上述的声明方法外,ES6
规范还新增了箭头函数,即可以通过以下的方法来声明一个函数:
1
2
3
4
const calc = (a,b)=>{
console.log(a+b)
};
calc(1,2)
同为动态语言,javascript
的函数形式与python
相似,其在function
的括号内填入参数且无需标明数据类型,返回值则在函数体内用return
返回。
1
2
3
4
const calc = function(a,b){
return a + b
};
console.log(calc(1,2))
注意,若在函数内没有return
语句,则默认return undefined
。
2.缺省值
在定义函数时,我们可以选择让其返回默认值。
在javascript
中,若未向函数传参,则该参数在函数内部会被定义为undefined
。
1
2
3
4
5
6
const JudgeName = function(name){
return name === undefined;
};
console.log(JudgeName())
//输出结果
true
通过这个特性,我们便可以实现设置默认值的效果。
1
2
3
4
5
6
7
8
9
10
11
12
const JudgeName = function(name){
if(name === undefined){
return "Outer"
}else{
return name
}
};
console.log(JudgeName())
console.log(JudgeName("Cyrex"))
//输出结果
Outer
Cyrex
除此之外,我们可以将其简化:
1
2
3
4
5
const JudgeName = function(name){
return name || "Outer"
};
console.log(JudgeName())
console.log(JudgeName("Cyrex"))
通过return result || default
的方式,仍然是可以实现默认值效果的,其原理是:
1
2
3
4
Default = "我是缺省值"
console.log(false || Default)
//输出结果
我是缺省值
除了上述的方法外,我们也可以直接在定义函数时给参数赋值
1
2
3
4
5
const JudgeName = function(name = "Outer"){
return name
};
console.log(JudgeName())
console.log(JudgeName("Cyrex"))
其原理是,在定义函数时已经给参数赋了值。若传参则新值会代替其默认值,若未传参则不变。
3.参数列表
在需要传入多个参数时,可以通过参数列表来全部接收,其语法与go
类似:
1
2
3
4
5
6
7
8
9
10
const sum = function(...list){
let total = 0
for (let a of list){
total += a
}
return total
}
console.log(sum(1,2,3,4,5,6,7))
//输出结果
28
...变量名
会将传入的数值转化为对应的数组,我们只需要在函数中调用这个数组即可。
此外,javascript
提供了arguments
,其指代的是输入的所有参数组成的参数列表。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
const sum = function(){
console.log(typeof arguments)
console.log(Array.isArray(arguments))
let total = 0
for (let a of arguments){
total += a
}
return total
}
console.log(sum(1,2,3,4,5,6,7))
//输出结果
Object
false
28
上述代码同样实现了将输入的所有参数进行求和。
且我们可以观察到arguments
的数据类型为Object
且其isArray
值为false
,证明其不是数组。
4.定义方法
如果在对象中声明函数,则该函数会变成对应对象的方法。
1
2
3
4
5
6
7
8
9
let obj = {
name : "Outer",
hello : function(){
console.log(`Hello,${this.name}`)
}
}
obj.hello()
//输出结果
Hello,Outer
上述函数实际是将函数定义给了对应的key
,当我们调用这个key
的时候,只需要加上()
即可实现对应方法的调用。
与函数的声明一样,我们可以通过多种方式来实现方法的声明。
我们可以直接给函数起名,而不使用匿名函数。
1
2
3
4
5
6
7
let obj = {
name : "Outer",
hello(){
console.log(`Hello,${this.name}`)
}
}
obj.hello()
也可以使用之前的箭头函数,但注意,一旦使用箭头函数,其内部不可以使用this
,之后讲到this
时会再做解释。
1
2
3
4
5
6
7
let obj = {
name : "Outer",
hello:()=>{
console.log("Hello!")
}
}
obj.hello()
由于不能使用this
的特性,一般不建议使用箭头函数来声明方法。
5.this
在方法的声明中内我们使用了this
,接下来将更加详细的介绍this
首先,一段javascript
的所有代码都被囊括在一个称为window
的对象中,其类似于HTML
的body
,是javascript
运行的主体。
1
2
3
window.alert("alert")
var MyName = "Outer"
console.log(window.MyName)
-
如果我们在方法外调用
this
,则其指代的对象便是window
。 -
如果在方法内使用
this
,则this
指代的是拥有该方法的对象。 -
如果在箭头函数内使用
this
,则指向的是window
如果我们在对象外定义了一个函数,则可以在该函数内设置this
,并手动将其指向我们指定的对象。
常见的方法有三种:call、apply、bind
关键字 | 用法 |
---|---|
call | function.call(this指向谁, 参数1,参数2...) |
apply | function.apply(this指向谁, [参数1, 参数2...]) |
bind | function.bind(指向,参数1,参数2,...) |
首先使用的是函数类型的call
方法,他可以将函数转化为指定对象的方法。
1
2
3
4
5
6
7
let newFunc = function(){
console.log(`Hello,${this.name}`)
}
let Obj = {
name:"Outer"
}
newFunc.call(Obj)
其传参的方法为,在call
内先填入要指向的对象,之后再填入参数
1
2
3
4
5
6
7
8
9
10
11
let newFunc = function(age){
this.age = age
}
let Obj = {
name:"Outer",
age:undefined,
}
newFunc.call(Obj,19)
console.log(Obj)
//输出结果
{name: 'Outer', age: 19}
其他两种方法的用法已经在表格内列举,其中apply
在传参时需要传入数组。
1
newFunc.apply(Obj,[19])
此外,bind
不同于call
的地方在于,其并不是直接调用方法,而是返回对应的方法,需要我们自行调用
1
2
3
4
5
6
7
8
9
10
let newFunc = function(age){
this.age = age
console.log(this)
}
let Obj = {
name:"Outer",
age:undefined,
}
newFunc.bind(Obj,19)()
newFunc.bind(Obj)(19)
其中,两种传参的方法都是正确的,可以在绑定对象时传参,也可以绑定完之后再传参。
注意,箭头函数没办法修改this
的指向。
二.对象
1.创建对象
在javascript
中,一共有4中创建对象的方式
其分别为,直接定义对象、new
关键字、原型对象以及create
方法。
之前在数据类型那一章节介绍的便是直接定义对象
1
2
3
const Obj = Object({
name:"Outer",
})
除了直接定义外,我们也可以通过new
关键字来生成一个新的对象:
1
2
3
const Obj = new Object({
name:"Outer",
})
注意,new
的用法是与数据类型连用,即new Type()
,其不仅可以定义Object
类型,也可以用来定义函数等:
1
2
3
const func = new function(){
console.log("Hello")
}
另外,new
是用于为变量实例化的,因此其后边接的一定是实例化的数据。
原型对象即通过定义一个函数,并引用该函数作为原型来创建新的对象,且该函数被称为构造函数,有点类似python
中的__init__
方法
1
2
3
4
5
6
7
8
9
10
11
function Student(name,age){
this.name = name
this.age = age
}
Student.prototype.Say = function(){
console.log(`${this.name} Says He is ${this.age}-Year-Old`)
}
const user = new Student("Outer",19);
user.Say()
//输出结果
Outer Says He is 19-Year-Old
上述代码中,我们创建了一个构造函数Student
,并在之后将其实例化,将实例化的内容赋值给了user
我们也可以使用匿名函数来实现这种效果,但一般不建议使用这种立即执行函数
1
2
3
4
5
6
const func = new function(name = "Outer"){
this.name = name
}
console.log(func)
//返回值
{name: 'Outer'}
prototype
可以为对象增加方法,如上方的Student.prototype.Say
便为Student
增加了Say
方法。
最后一种方法是create
方法,其并不是直接创建一个对象,而是将内容传给一个空的对象的prototype (原型)
字段内
1
2
3
4
5
6
7
8
9
10
11
const user = Object.create({
name :"Outer",
age : 19,
})
console.log(user)
//输出结果
{}
prototype:{
name:"Outer",
age:19,
}
原型顾名思义,即可以理解为当前创建的这个对象是prototype
的副本。
如果没有内容,该对象就会继承其原型的内容。
2.更改属性
此处说的属性,即字段、键。字段是在将对象看做结构体而言的,键是在将对象看做键值对而言的。
一个对象的属性被分为两类,一类是继承而来的,被称为prototype
型,另一类是该对象自身的属性,是非prototype
的,可以是可枚举属性,也可以是不可枚举属性。
关键字 | 作用 |
---|---|
delete | 删除自身属性,不能删除prototype 型属性 |
in | 可以判断自己是否拥有该属性,可以为prototype 型 |
hasOwnProperty | 只能检测非prototype 属性,不包含prototype 型 |
for in | 枚举属性,包括非prototype 属性和prototype 型 |
Object.keys() | 只能枚举非prototype 属性 |
在之前介绍过delete
可以删除自身的某个字段,但不能删除prototype
型字段。
in
关键字可以查询对象中是否存在指定字段。
1
2
3
4
5
6
7
8
9
10
const user = Object.create({
name :"Outer",
age : 19,
});
user.sex = "Male";
console.log("age" in user)
console.log("sex" in user)
//输出结果
true
true
可以看到,无论是create
创建的prototype
属性,还是自身定义的属性,都可以被in
检测。
与之不同的是,hasOwnProperty
不会检测prototype
属性
1
2
3
4
5
6
7
8
9
10
const user = Object.create({
name :"Outer",
age : 19,
});
user.sex = "Male";
console.log(user.hasOwnProperty("Outer"))
console.log(user.hasOwnProperty("sex"))
//输出结果
false
true
for in
在之前介绍数组时已经说过,可以用来枚举对象的属性。
1
2
3
4
5
6
7
8
9
10
11
12
const user = Object.create({
name :"Outer",
age : 19,
});
user.sex = "Male";
for(let index in user){
console.log(index)
}
//输出结果
name
age
sex
与之区分的是Object.Keys()
不会返回prototype
属性,且其返回值是一个string
类型的数组。
1
2
3
4
5
6
7
8
const user = Object.create({
name :"Outer",
age : 19,
});
user.sex = "Male";
console.log(Object.keys(user))
//输出结果
['sex']
上述所有的可以列举prototype
型的方法都能检测对象里的方法。
比如下边用for in
来测试:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const user = Object.create({
name :"Outer",
age : 19,
Say:function(){
console.log("Hello!")
}
});
user.sex = "Male";
for(let index in user){
console.log(index)
}
//输出结果
name
age
sex
Say
3.API
介绍一下Obejct
上的常用API
。API
即一些预先定义的函数,目的是提供访问一组例程的能力,而又无需访问源码,或理解内部工作机制的细节。
先前介绍数组时的Array.isArray
实际上就是一种API
,只是那时暂时被称为函数。
API | 作用 |
---|---|
Object.assign() | 浅拷贝,只能拷贝非prototype 属性 |
Object.keys() | 获取可枚举的非prototype 属性的字段名并返回一个数组 |
Object.getOwnPropertyNames() | 获取自身拥有的枚举或不可枚举的非prototype 属性的字段名并返回一个数组 |
Object.defineProperty() | 可以对对象的属性调整设置。 |
关键介绍一下浅拷贝Object.assign()
,浅拷贝的拷贝体现在他可以将某个对象的内容拷贝进目标空对象内。其语法为:
1
Object.assign(target,...sources);
如:
1
2
3
4
5
const obj = {}
Object.assign(obj,{"name":"Outer"})
console.log(obj)
//输出结果
{"name":"Outer"}
Object.assign
有几个特点:
- 只能拷贝非
prototype
属性到目标空对象 - 如果目标对象已有字段,则拷贝会将目标对象原有的同名属性覆盖,而不重名的不会被删除。
- 如果源对象内含有一个子对象,且被拷贝给了目标对象,这个过程实际是给目标对象拷贝了这个子对象的引用。(注意:数组也是一种对象,因此也受此影响)
如,目标方法已有name
字段,再次拷贝时便会将同名属性覆盖。
1
2
3
4
5
const obj = {"name":"Outer"}
Object.assign(obj,{"name":"Cyrex"})
console.log(obj)
//输出结果
Cyrex
第三个特点字面不易理解,通过例子来解释:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const obj = {}
const source = {
name:"Outer",
subObj:{
age:19,
}
}
Object.assign(obj,source)
console.log(obj)
source.subObj.age = 20
console.log(obj)
//输出结果
name: "Outer"
subObj: {age: 20}
name: "Outer"
subObj: {age: 20}
在上述代码中,我们将带有子对象subObj
的源对象source
拷贝给了obj
,我们会发现,尽管更改的是source
的属性,obj
的subObj.age
属性同样发生了更改。这便是传引用导致的。
且该效果是全局的,如上述代码中第一个console.log
在更改之前,输出结果也仍然是更改之后的数据。
之后的Object.keys()
和Object.getOwnPropertyNames()
不再过多介绍,前者在之前已经介绍过,后者与前者不同的是其可以获取不可枚举属性。
至于不可枚举属性的设置,则需要Object.defineProperty()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
const o1 = {name: "Outer"}
Object.defineProperty(o1, "age", {
value: 21,
writable: true,
enumerable: false,
configurable: true,
})
console.log("Object.keys", Object.keys(o1))
console.log("Object.getOwnPropertyNames", Object.getOwnPropertyNames(o1))
//输出结果
['name']
['name','age']
其中,defineProperty
的内容分别为
属性 | 作用 |
---|---|
value | 设置该字段的值 |
writable | 是否可更改 |
enumerable | 是否可枚举 |
configurable | 是否可删除 |
4.原型与原型链
a.原型
在javascript
中,每个对象都有一个隐藏的[[prototype]]
字段(除了Null
和Object.prototype
)。
关键字 | 作用 |
---|---|
[[prototype]] |
一个非null 且非Object 的对象的原型字段名 |
prototype |
指向一个构造函数抽象出的虚拟对象,即该构造函数实例化后的对象的原型 |
__proto__ |
直接指向一个对象的原型 |
首先要区分[[prototype]]
与prototype
,前者是一个对象自带的内部属性,而后者是函数对象的属性,该对象被用作通过该函数创建的实例的原型。
1
2
3
4
5
6
7
const source = {
name:"Outer"
}
const obj = Object.create(source)
console.log(Object.getPrototypeOf(obj) === source);
//输出结果
true
此处Object.getPrototypeOf()
的API
会获取对应对象的[[prototype]]
字段的内容。
1
2
3
4
const obj = {}
console.log(Object.getPrototypeOf(obj) === Object.prototype)
//输出结果
true
可以看出,如果在创建时未指定其[[prototype]]
的指向,则会默认指向Object.prototype
而prototype
通常是针对原型函数而言的,.prototype
会获取以原型函数实例化的对象的原型。
1
2
3
4
5
6
7
8
function Person(name, age) {
this.name = name;
this.age = age;
}
var Outer = new Person("Outer", 19);
console.log(Object.getPrototypeOf(Outer) === Person.prototype);
//输出结果
true
上述代码中,Person.prototype
实际是就是以原型函数Person
来实例化的对象Outer
的原型,类似于将该原型函数给抽象成一个虚拟的对象。
1
Object:constructor: ƒ Person(name, age)
上述代码便是所谓抽象出来的对象。
实际上,如果想访问一个对象的[[prototype]]
,应该通过__proto__
取检索访问
1
2
3
4
5
6
7
const source = {
name:"Outer"
}
const obj = Object.create(source)
console.log((obj.__proto__)===source)
//输出结果
true
总而言之,.prototype
能够索引到构造函数抽象出的虚拟对象,而不能获取任意对象的[[prototype]]
字段。能够获取任意对象的[[prototype]]
字段的是__proto__
属性。
b.原型链
每个非空非Object
的对象都有其对应的原型,这些原型组成一条链表,便是原型链。原型链的末尾是Null
1
2
3
4
const source = {
name:"Outer"
}
const obj = Object.create(source)
如上述代码构建的原型链即为:
1
obj -> source -> Object -> Null
c.原型更改
我们可以通过Object.create()
的API
来为某个对象添加方法。
1
2
3
4
5
6
const obj = Object.create({
Say:function(){
console.log("Hello!")
}
})
obj.Say()
除此之外,我们可以为其原型增添或更改其字段或方法。
1
2
3
4
5
const obj = {}
Object.prototype.Say = function(){
console.log("Hello!")
}
obj.Say()
或者通过__proto__
来访问一个对象的原型,并进行更改
1
2
3
4
5
6
7
8
9
10
const source = {
name:"Outer"
}
const obj = Object.create(source)
obj.__proto__.name = "Cyrex"
console.log(obj)
//输出结果
obj -> source{
name:"Cyrex",
} //这里进行了抽象表达来展现原型链
有了上述知识,我们便可以解释new
的过程了:
- 创建一个新的对象
- 把该对象的
__proto__
属性设置为构造函数的prototype
属性,即完成原型链 - 执行构造函数中的代码,构造函数中的
this
指向该对象 - 返回该对象obj
三.时间库
1.时间戳
在javascript
中获取时间戳需要使用new Date().getTime()
其起始时间是1970-01-01 00:00:00
,即Unix
纪元
1
2
3
console.log(new Date().getTime())
//输出结果
1721745613633
注:这个时间戳是毫秒级别的13位时间戳
时间戳与时间的转换可以通过Date()
来实现
1
2
3
4
const timer = new Date(1721745613633)
console.log(timer)
//输出结果
Tue Jul 23 2024 22:40:13 GMT+0800 (中国标准时间)
也可以直接通过new Date()
来获得标准时间
1
2
3
console.log(new Date())
//输出结果
Tue Jul 23 2024 22:42:00 GMT+0800 (中国标准时间)
2.日期对象
与其他语言的时间库一样,我们可以将Date()
的时间进行拆分
1
2
3
4
5
6
7
8
9
10
11
12
13
14
const date = new Date()
console.log(date.getFullYear())
console.log(date.getMonth()+1)
console.log(date.getDate())
console.log(date.getHours())
console.log(date.getMinutes())
console.log(date.getSeconds())
//输出结果
2024
7
23
22
58
17
其中要注意的是,data.getMonth()
获取的月份是从0开始的,因此一般要进行date.getMonth()+1
来获取正确的月份。
3.格式化输出
通过javascript
的字符串格式化,我们可以轻松实现格式化输出
1
2
3
4
5
6
7
8
9
10
11
12
13
const nowTime = function(){
let timer = new Date()
let y = timer.getFullYear()
let M = timer.getMonth() + 1
let d = timer.getDay()
let h = timer.getHours()
let m = timer.getMinutes()
let s = timer.getSeconds()
return `${y}-${M}-${d} ${h}:${m}:${s}`
}
console.log(nowTime())
//输出结果
2024-7-2 23:8:12
但这样不符合我们常规的时间表达方式,通常而言我们要在不足两位时补足0,而不是直接写1位。
1
2
3
4
5
6
7
8
9
10
11
12
13
const nowTime = function(){
let timer = new Date()
const y = timer.getFullYear().toString()
const M = (timer.getMonth() + 1).toString().padStart(2, "0")
const d = timer.getDate().toString().padStart(2, "0")
const h = timer.getHours().toString().padStart(2, "0")
const m = timer.getMinutes().toString().padStart(2, "0")
const s = timer.getSeconds().toString().padStart(2, "0")
return `${y}-${M}-${d} ${h}:${m}:${s}`
}
console.log(nowTime())
//输出结果
2024-07-23 23:18:48
在javascript
中,存在padStart()
方法可以将字符串进行补零处理,但其必须要在字符串后,因此需要toString()
将Date()
返回的数字转化为字符串,然后再进行补零处理。
1
padStart(num,"ch")
其中num
是需要补的位数,如果不足num
则会在前边补ch
直至该字符串的长度等于num
4.时间计算
通过new Date()
类型自带的方法set
可以自定义其时间
与get
类似,set
需要加上对应的时间名
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
let timer = new Date()
timer.setFullYear(timer.getFullYear() - 1)
timer.setMonth(timer.getMonth())
timer.setDate(timer.getDate() - 1)
timer.setHours(timer.getHours() - 1)
timer.setMinutes(timer.getMinutes() - 1)
timer.setSeconds(timer.getSeconds() - 1)
const nowTime = function(timer){
const y = timer.getFullYear().toString()
const M = (timer.getMonth() + 1).toString().padStart(2, "0")
const d = timer.getDate().toString().padStart(2, "0")
const h = timer.getHours().toString().padStart(2, "0")
const m = timer.getMinutes().toString().padStart(2, "0")
const s = timer.getSeconds().toString().padStart(2, "0")
return `${y}-${M}-${d} ${h}:${m}:${s}`
}
console.log(nowTime(timer))
如上,我们便将每一个时间单位都进行了减一处理。
除了这种计算外,如果我们有两个不同的时间戳,我们可以对其进行相减获得相差的毫秒数
1
2
3
4
5
6
7
8
9
10
11
let Timestr = new Date().getTime() + 2313141241
let now = new Date()
let then = new Date(Timestr) //假定Timestr是一个时间戳
let diff = then - now
let s = diff / 1000
let m = s / 60
let h = m / 60
console.log(h)
//输出结果
642.5392336111111