前端基础学习之JavaScript part two

Posted by OuterCyrex on July 23, 2024

二.函数、对象与时间库

一.函数

1.函数的声明

javascript中,函数的声明与go类似,其关键字为function

1
2
3
4
5
6
function hello() {
    console.log("Hello")
};
hello()
//输出结果
hello

同样,在javascript中也存在匿名函数,在javascriptgo中,函数被视为一等公民,可以进行赋值给参数等多种操作。

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对象中,其类似于HTMLbody,是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上的常用APIAPI即一些预先定义的函数,目的是提供访问一组例程的能力,而又无需访问源码,或理解内部工作机制的细节

先前介绍数组时的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的属性,objsubObj.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]]字段(除了NullObject.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的过程了:

  1. 创建一个新的对象
  2. 把该对象的__proto__属性设置为构造函数的prototype属性,即完成原型链
  3. 执行构造函数中的代码,构造函数中的this指向该对象
  4. 返回该对象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