April 30, 2024

JavaScript学习笔记

JavaScript学习笔记

1 JavaScript引入

1.1 在网页上输出 Hello world

JavaScript代码需要放在HTML文件中,下面是一个简单的HTML代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Debussy</title>
</head>
<body>
<script src="source/js/test.js"></script>
</body>
</html>

可以在.html文件中的script标签中书写JavaScript代码,但是一般采用下面的方式进行外部引入。

1
<script src="source/js/test.js"></script>

该标签需要放在body标签的最后,以加快网页的加载速度。不要使用合并标签。在test.js文件中书写下面的代码调用一个弹窗,显示Hello world!

1
alert("Hello world!");    // 在弹窗中输出Hello World!

alert函数的参数是一个字符串,字符串可以用单引号引出,也可以用双引号。这一点和C语言不同。

弹窗命令可以用来方便地调试程序。前端写弹窗非常简单,至少相比于后端要简单不少。

1.2 JavaScript 简介

JavaScript是一门弱脚本语言(可能是世界上最流行的脚本语言),源代码不需要通过编译,而是由浏览器解释运行,用于控制网页的行为。JavaScript只能在浏览器引擎上运行,浏览器引擎类似发动机引擎,JavaScript相当于汽油,而浏览器本体相当于一台汽车。我们可以用汽油和发动机去驱动汽车,也可以去浇灌农田。类似地,使用JavaScript可以实现网页端的开发,在不同的运行环境下(应用场景不同)也可以发生不同的作用,例如使用Electron(NodeJS)框架实现桌面端应用的开发。

只掌握JavaScript只能说掌握了前端开发的基础。一般的前端开发可以分为原生开发和框架开发,说白了,就是是否重复造轮子的选择问题,或者说是否造好看的轮子的问题。目前流行的前端框架有Vue,UI框架有阿里巴巴出品的Ant-Design等。

==JavaScript的线上环境和开发环境可能不一致==:ECMAScript是JavaScript遵循的标准,最新已经更新到ES6了。但是大多数浏览器支持仍然停留在ES5,所以需要WebPack等工具将ES6代码打包成ES5,才能在大多数浏览器上运行。

1.3 CSS 和 CSS 预处理器

这一块知识和JavaScript关系不大,但是也记录在此,因为它和前端开发结合的相当紧密。

CSS层叠样式是一门标记语言,它本身并不是编程语言,它不支持自定义变量和引用,不具备任何语法支持特性。这成为了它的主要缺陷,CSS的语法不够强大,不能嵌套书写,模块化开发中需要写很多重复的选择器;没有合理的样式复用和维护机制,逻辑上相关的属性必须以字面量的形式重复输出,导致难以维护。为了解决这一问题,实际开发中通常使用一种称为CSS预处理器的工具。

CSS预处理器定义了一种新的语言,它的基本思想是,用一种专门的编程语言,为CSS增加一些编程的特性。将CSS作为目标生成文件(编译目标),而开发者需要书写生成CSS语言的逻辑就可以方便地实现网页样式的维护和更新。

常用的CSS预处理器由SASS和LESS,二者相比LESS更加简单,更常用。LESS基于NodeJS,功能比SASS简单,但是解析效率不如SASS。实际开发中LESS也够用了。

2 基本语法入门

2.1 变量

JavaScript中定义变量非常简单,甚至不需要显式地指定变量类型,统统用var表示。变量名中可以添加$,也可以以它开头,只不过不常用。甚至可以使用中文命名和赋值,不过应该和使用的中文编码方式有关,容易报错,感觉还是不要使用为妙。

1
2
3
var a = 1;
var b = 2;
var str = "Hello world!";

2.2 条件判断

1
2
3
4
5
6
7
8
9
10
11
var score = 80;

if (score > 60 && score < 90) {
alert("合格");
}
else if (score >= 90) {
alert("优秀");
}
else {
alert("不合格");
}

单条语句可以省略花括号。

2.3 基本的浏览器操作

调试除了用alert()或者console.log()函数输出信息外,还可以断点调试。这里以Edge浏览器为例。

点击F12打开控制台,在源代码处可以打断点。刷新网页即可直接运行。在右侧监视窗口可以添加变量名用于监视变量的变化。

除此之外,常用的窗口及其作用如下所示:

2.4 常见数据类型

在网页上常见的数据类型不止数字和字符串。图像,文本,音频,视频等都是数据类型。JavaScript的数据类型非常庞杂。

2.4.1 数字 Number

JavaScript不区分小数和整数,所有数字统一都是number类型。JavaScript支持直接表示整数、浮点数、负数、科学计数法(例如1.23e3)、NaNInfinity(无穷大)。所有数字都是number类型。

2.4.2 字符串

可以用单引号引出,也可以用双引号引出。

2.4.3 布尔值

包含true和false,没有什么特别。

2.4.4 逻辑运算

&&||!

2.4.5 判等运算符

等于和绝对等于可以看作JavaScript的一个缺陷,坚持一定使用绝对等于进行比较。下面是一些注意事项:

2.4.6 空指针和未定义

空指针null没有什么稀奇,但是未定义undefined在其他语言中并不常见。例如此时有一个数组,如果要打印的数组元素超过了数组的长度,JavaScript不会报错,而是会返回一个undefined类型。

2.4.7 数组

JavaScript对变量的类型没有明确定义,那么对于数组中是否存储相同类型的元素也不关心。JavaScript中的数组可以存放任意类型的变量。数组可以使用[]或者使用Array()方法定义,但是使用方括号对于代码的可读性贡献更好。

1
2
3
var arr = [1, 2, 3, 4, "hello", null, true, undefined];
// 也可以采用下面的方式定义一个数组
var arr2 = new Array(1, 2, 3, 4, "world", undefined);

2.4.8 对象

JavaScript中定义一个对象可以直接使用大括号{}定义。任何数据类型都要用var定义,对象也不例外。这里的对象并不是面向对象的概念,没有封装、继承、多态等概念

1
2
3
4
5
var person = {
mame: "Include everything",
age: 20,
tags: ["Piano", "C", "Verilog"]
};

注意:JavaScript中定义对象不需要先定义一个类。内部变量通过键值对的方式给出,值同样可以是不同的类型,具体的格式和json格式非常相似,每个属性之间使用逗号分隔,最后一个键值对不需要加逗号。使用类似person.name的方式访问元素即可,没有什么稀奇。

2.5 严格检查模式

由于JavaScript中对于变量的定义十分宽泛,甚至直接使用类似Python语法定义变量都不会报错:

1
i = 0;

实际上,上面的语句将i定义为一个全局变量。如果采用全局变量,会导致代码之间程序耦合的问题,会带来很多毁灭性的bug。在ES5中,采用var定义的变量是局部变量,而在ES6中,使用letconst定义变量才会被当作局部变量。

1
2
let a = 0;
const b = 1;

在JS代码之前加入语句'use strict';语句即可开启严格检查模式,写这一行代码必须写在第一行。编译器会检查变量是否被定义,未经定义的变量会直接报错。使用后还可以规避一些JavaScript中变量定义随意性导致的问题。

1
2
3
'use strict';

a = 1;
image-20240430230041181

2.6 分支与循环

分支循环的语法基本和C语言一模一样。注意某些特殊的写法如for ... in ...语句或者某些对象的循环方法即可(例如forEach())。

3 数据类型详解及其属性和方法

3.1 字符串

正常的字符串使用单引号或者双引号引用。需要注意转义字符的引用,例如\n\'\r\t等。

转义字符甚至可以直接打印ASCII编码字符和UniCode字符:

1
2
console.log('\u4e2d');    // 中
console.log('\x41'); // A

JavaScript支持多行字符串编写,多行字符串需要使用`字符引用:

1
2
3
let msg = `hello
world
你好`;

在ES6中,模板字符串用下面的格式进行书写,注意使用多行字符串符号引用。(ES6新特性)

1
2
3
4
let name = 'Include everything';
let age = 20;

let msg = `Hello, ${name}, your age is ${age}`;

字符串是不可变型变量。直接修改字符串中的某个字符不会报错,但是修改也不生效。

1
2
3
4
let name = 'Zhang Yun';
console.log(name[0]);
name[0] = 'A';
console.log(name); // 仍然打印 Zhang Yun

下面是一些字符串常用的属性和方法:

1
2
3
4
5
6
7
8
9
let name = 'Zhang Yun';

console.log(name.length);
console.log(name.toUpperCase());
console.log(name.toLowerCase());

console.log(name.indexOf('n')); // 返回找到第一个该字符的下标
console.log(name.substring(1)); // 截取第一个字符之后所有的字符
console.log(name.substring(1, 3)); // 截取字符串,截取范围是[1, 3)

3.2 数组

JavaScript中的数组和其他语言有相同的地方,比如都可以使用下标访问元素。但是也有不同的地方:

1
2
3
4
5
6
let arr = [1, 2, 3, 4, 5];

arr.length = 8;
console.log(arr);
arr.length = 2;
console.log(arr);

下面给出数组的一些常用方法:

1
2
3
4
let arr = [1, 2, 3, 4, 5, "1", "2"];

console.log(arr.indexOf(1)); // 0
console.log(arr.indexOf("1")); // 5,数组中数字和字符串不同
1
2
3
4
let arr = [1, 2, 3, 4, 5];

console.log(arr.slice(2)) // [3, 4, 5]
console.log(arr.slice(2, 4)) // [3, 4],截取范围是[a, b),和substring类似,返回一个数组
1
2
3
4
5
6
let arr = [1, 2, 3, 4, 5];

arr.push('a', 'b') // 压入尾部
arr.pop() // 弹出尾部
arr.unshift('head1', 'head2') // 压入头部
arr.shift() // 弹出头部
1
2
3
4
let arr = ['B', 'C', 'A'];

arr.sort(); // 正序
arr.reverse(); // 逆序
1
2
3
let arr = ['B', 'C', 'A'];

console.log(arr.concat([1, 2, 3])); // ['B', 'C', 'A', 1, 2, 3]
1
2
3
let arr = ['B', 'C', 'A'];

console.log(arr.join('-')); // 'B-C-A'

数组还有一个方法,可以实现类似循环的操作:forEach,里面的参数是一个方法,方法的参数即为数组的元素。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
let arr = [3, 2, 44, 32, 1, 2];

arr.forEach(function (value){
console.log(value);
})

// for (index in object)
for (let i in arr) {
console.log(arr[i]);
}

// for (element of object)
for (let i of arr) {
console.log(i);
}

for in遍历数组下标还存在一个特性,它不仅会返回元素的下标,还会返回元素对应的键:

1
2
3
4
5
6
let arr = [1, 2, 3, 4, 5];
arr.name = 'debussy'; // arr 会新增一个键值对对象
console.log(arr);
for (let x in arr) {
console.log(x); // 打印1, 2, 3, 4, 5和 name
}

3.3 对象

JavaScript中的对象用键值对的方式书写。这里的对象不是面向对象中的对象概念,没有封装、继承、多态的概念。所有的键都是字符串,值可以是任意类型

可以使用delete命令动态删除对象中的某一属性。

1
2
3
4
5
6
7
8
let person = {
name: "Debussy",
age: 20,
score: 80
};
delete person.score;
console.log(person);
console.log(person['age'])
image-20240501143302426

直接给不存在的属性赋值,不会报错,JavaScript会自动添加属性。

1
person.tags = ['C', 'Verilog'];
image-20240501143432663

使用in关键字判断属性是否在这个对象中:

1
2
console.log('age' in person);    // true
console.log('toString' in person); // true, 'toString'是父类的属性, person继承了父类的属性

判断属性是否属于自身:

1
2
console.log(person.hasOwnProperty('toString'));    // false
console.log(person.hasOwnProperty('name')); // true

对象定义中如果后面的定义和前面的定义冲突,那么以后面的为准(后面的定义会覆盖前面的):

1
2
3
4
5
6
7
8
9
10
11
12
13
let Debussy = {
name: "Debussy",
age: 20,
isMarried: false,
skills: ["HTML", "CSS", "JS"],
greet: function() {
console.log("Hello, my name is " + this.name);
},

name: "Include everything"
}

Debussy.greet(); // Hello, my name is Include everything

3.4 Map和Set

Map和Set是ES6的新特性。Map在组成上很像Python的字典,由键值对组成。定义的时候可以通过一个二维数组来定义。

1
2
3
4
5
6
7
8
9
10
11
12
13
let map = new Map([["tom", 90], ["bob", 80], ["alice", 70]]);

let score = map.get("tom"); // get方法获取键值对的值
console.log(score);

map.set("tom", 100); // 设置已有键值对的值
console.log(map);

map.set("Debussy", 85); // 通过set创建新键值对
console.log(map);

map.delete("tom"); // 删除键值对
console.log(map);

Set是一个无序不重复的集合。一般只能用来判断某元素是否在Set中存在。定义时使用一个数组定义,但是多余的元素会被自动忽略。

1
2
3
4
5
6
7
8
9
let set = new Set([1, 1, 1, 3]);
console.log(set); // set去重
console.log(set.size); // 2

set.add(2); // set添加元素
console.log(set); // {1, 2, 3}
set.delete(1); // set删除元素
console.log(set); // {2, 3}
console.log(set.has(3)); // set判断元素是否存在,返回true

3.5 iterator 迭代器

迭代器是Python中一个经典的概念,JavaScript中也有类似的结构。迭代器是ES6的新特性,迭代器的主要作用是遍历对象中所有的元素,实际上很多数据结构都可以自己写函数或者方法来实现迭代器的功能,但是利用迭代器可以简化代码书写,提高效率。

在数组中可以使用for infor of迭代所有的元素,前者迭代下标,后者迭代元素:

1
2
3
4
5
6
7
8
9
// for (index in object)
for (let i in arr) {
console.log(arr[i]);
}

// for (element of object)
for (let i of arr) {
console.log(i);
}

for of这种方法在Map和Set中也同样适用。只有支持迭代器的对象才可以使用for of遍历。

1
2
3
4
5
6
7
8
9
10
let map = new Map([["tom", 90], ["bob", 80], ["alice", 70]]);

for (let x of map) {
console.log(x); // 以数组的方式返回所有的键值对
}

let set = new Set([1, 1, 1, 3]);
for (let x of set) {
console.log(x);
}

4 函数和面向对象

方法是一种函数。对象内定义的函数称为方法,其他函数称为普通函数。方法和函数的格式相同,只是二者的定义的位置不同。

4.1 函数

下面是一个简单的返回绝对值的函数:

1
2
3
4
5
6
function abs(x) {
if (x >= 0)
return x;
else
return -x;
}

如果函数没有正常返回,或者传参错误,JavaScript会返回NaN或者Undefined,而不是报错。

image-20240502111921868

函数也可以使用下面的方式定义,定义一个匿名函数,并且将该函数赋值给一个变量,这个变量就会称为该函数的名称,通过abs就可以调用函数。两种函数定义的方式等价。

1
2
3
4
5
6
let abs = function (x) {
if (x >= 0)
return x;
else
return -x;
}

JavaScript的参数的个数甚至都可以不确定。可以传递任意个参数,甚至不传递参数,函数在运行时都不会报错。这就需要程序员在设计函数时考虑到这些异常。

如果传入参数不存在,或者类型错误,可以使用typeof关键字获取参数类型,并通过throw关键字抛出一个异常:

1
2
3
4
5
6
7
8
9
10
let abs = function (x) {
if (typeof x !== "number") {
// throw "x is not a number!!!";
throw new Error("x must be a number"); // 该种写法会返回错误出现的位置
}
if (x >= 0)
return x;
else
return -x;
}
image-20240502113736228

arguments是JavaScript免费赠送的关键字,它会获取传入函数的所有参数,并且返回一个对应的数组:

1
2
3
4
5
6
7
8
9
10
let abs = function (x) {
console.log("x = " + x);
for (let i = 0; i < arguments.length; i++) {
console.log("arguments[" + i + "] = " + arguments[i]);
}
if (x >= 0)
return x;
else
return -x;
}

使用arguments会有一个问题:它包含所有的参数,但是有时我们又恰恰想用剩余的参数来进行操作。这时可以使用rest关键字(ES6新特性)获取除了已经定义的参数之外的所有参数。

如果不使用缺省参数关键字...获取剩余的参数:

1
2
3
4
5
6
7
8
9
function func(a, b) {
console.log("a = " + a);
console.log("b = " + b);
if (arguments.length > 2) {
for (let i = 0; i < arguments.length; i++) {
...
}
}
}

如果使用...关键字,注意要在参数栏写明可能有缺省参数,且它只能写在最后,必须使用...标识表明rest是缺省的,...后是缺省参数的数组名称:

1
2
3
4
5
function func(a, b, ...rest) {
console.log("a = " + a);
console.log("b = " + b);
console.log(rest);
}

经过测试,rest并不是一个关键字,它相当于承载剩余参数的一个变量名而已,该变量是一个数组。

4.2 变量的作用域

4.2.1 函数之间变量作用域

JavaScript中的变量虽然可以在首次赋值时默认被直接声明,但是不可以在未定义时赋值给其他变量。下面的程序会立即报错,因为x = x + 1中等号右边的x没有被定义。可见使用var定义的变量是有作用域的。

1
2
3
4
5
6
function func1() {
var x = 1;
x = x + 1;
console.log(x);
}
x = x + 1;

如果两个函数中使用了相同的变量名,则两个变量名互不影响。

1
2
3
4
5
6
7
8
9
10
11
12
function func1() {
var x = 1;
x = x + 1;
console.log(x);
}
function func2() {
var x = 'A';
x = x + 1;
console.log(x);
}
func1(); // 输出2
func2(); // 输出A1(返回一个字符串,而不是返回B)

JavaScript的函数声明或者函数体甚至可以写在函数内部。下面的表达依然是合法的。并且内部函数可以使用外部函数的变量,但是外部函数不能访问内部函数的变量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 外部函数可以访问内部函数变量,反之不可以
function func_outer()
{
var a_outer = 1;
function func_inner()
{
var a_inner = 2;
console.log(a_outer);
console.log(a_inner);
}
func_inner();
console.log(a_inner); // ReferenceError: a_inner is not defined
}
func_outer();

如果内部函数和外部函数声明了相同的变量名,则内部变量使用内部变量,外部变量使用外部变量。虽然从结果上看互不影响,但是官方的说法是:JavaScript函数查找变量从自身函数开始,从内向外查找。假设外部存在这个同名变量,则内部变量屏蔽外部函数变量。

4.2.2 同一函数先后定义变量作用域

JavaScript中,虽然使用未定义的变量给其他变量赋值会导致程序报错,但是如果这个变量在后面的程序段中定义,则不会报错,而是会将定义提前,但是定义时的赋值并不会提前。这时使用这个变量会返回一个undefined。也可以理解为编译器会自动将所有变量的定义提前,但是赋值并不会提前。所以通用的做法是将所有变量写在程序的头部,以预防这种情况的发生。这是在JavaScript建立之初就存在的特性,有一定的优点,但是不利于代码维护。

1
2
3
4
var x = 1 + y;
console.log(x); // 1 + undefined = NaN
var y = 10; // 如果写y = 10则会直接报错,因为y = 10不是一个定义
console.log(y);

4.2.3 全局作用域和window对象

JavaScript中,window是一个特殊的对象,它代表浏览器,同时也是JavaScript的唯一的全局作用域。直接定义的变量都是全局变量,同时也是window的子属性。有些函数可以直接调用,例如alert(), 它是一个全局函数,它是window对象的子方法。==任何变量(函数也可以视为变量)如果没有在函数的作用范围内找到,就会向外查找,直到全局作用域window。如果在全局作用域都没有找到才会报错。==

1
2
3
4
5
6
var x = "Hello!";

// 下面三种写法等价
alert(x);
window.alert(x);
window.alert(window.x);

JavaScript中的变量可以是任意类型,当然也可以是一个函数。我们当然可以使用一个与原有函数同名的变量,将另一个函数赋值给这个变量,则原函数就会被新函数覆盖。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var x = "Hello!";
var y = "World!";

window.alert(x);

var old_alert = window.alert;
window.old_alert(x);

var alert = function () {

}
window.alert(y); // 失效,浏览器不会弹出窗口

alert = window.old_alert;
window.alert(x); // 重新有效

所有的全局变量都会绑定到window对象下,包括不同文件中定义的全局变量。假设不同文件中定义了相同的变量,就会导致冲突。解决方法是在每个JS文件中定义一个特殊的独一无二的对象,将这个对象作为当前文件的全局空间,可以在一定程度上减少冲突。

1
2
3
4
5
6
7
8
9
10
var Debussy = {};  // 当前文件的全局变量

// 定义变量
Debussy.name = "Debussy";
Debussy.age = 20;

// 定义方法
Debussy.add = function (a, b) {
return a + b;
};

4.2.4 局部作用域let和常量const

letconst都是ES6的新特性。现在已不建议使用var关键字定义常量,否则可能出现这样的Bug(特性?):

1
2
3
4
5
6
7
function func1 () {
for (var i = 0; i < 10; i++) {
console.log(i);
}
console.log(i); // 打印出了10,i是函数作用域下的变量,所以可以打印出来
}
func1();

但是这样的特性很多时候是不利于代码编写的。所以使用关键字let定义i就不存在这样的问题。

1
2
3
4
5
6
7
function func1 () {
for (let i = 0; i < 10; i++) {
console.log(i);
}
console.log(i); // ReferenceError: i is not defined
}
func1();

const的官方名是只读变量,它只可以在定义时被赋值,之后都不能被赋值。const声明的变量不可修改,数组和对象除外(数组和对象仅仅不能全部更改)。

1
2
3
const year = 2024;  // 使用const必须要在声明时就初始化数值
// year = 2023;
console.log(year);

4.3 方法的定义和调用

JavaScript中的方法定义十分简单。放置在对象中的函数就称为方法,甚至方法定义可以直接写在对象中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
let Debussy = {
name: 'Debussy',
age: 20,
height: 170,
weight: 60,
isMarried: false,
hobby: 'Music',
sayHello: function() {
console.log('Hello!');
},
sayName: function() {
console.log(this.name); // this指向当前对象
console.log(this);
}
}
Debussy.sayHello();
Debussy.sayName();

在对象中只有两种元素:属性和方法。this始终指向调用它的对象。如果将方法采用下面的写法,同样可以定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
let Debussy = {
name: 'Debussy',
age: 20,
height: 170,
weight: 60,
isMarried: false,
hobby: 'Music',
sayHello: function() {
console.log('Hello!');
},
sayName: getName
}

function getName() {
console.log(this.name);
}

Debussy.sayHello();
Debussy.sayName(); // Debussy

getName(); // 无法正常输出

在其他编程语言中,一般而言this指向的对象都不可更改,但是在JavaScript中可以通过apply()函数定义当前调用这个函数的对象。所有的函数对象都有apply方法。例如,将上面代码块中的getName()改为下面的形式就可以正常输出:

1
2
getName.apply(Debussy);
getName.apply(Debussy, []); // 空参数可以不写,也可以用一个空数组代替

4.4 原型继承和类继承

4.4.1 原型继承

早期的JavaScript程序并没有类定义,所以只能采用这种方法。如果一个对象想要获得另一个对象的部分属性,需要指定对象的__proto__属性来继承原有的对象,即指向一个原型。这样的编程方法虽然实现了类似面向对象的效果,但是没有强调面向对象的概念。

1
2
3
4
5
6
7
8
9
10
11
12
13
let Student = {
name: 'Debussy',
age: '20',
run: function(){
console.log(this.name + ' is running.');
},
}

let PrimaryStudent = {
__proto__: Student, // 通过__proto__属性,继承Student
name: 'xiaoming',
grade: '1',
}

如果需要更改继承对象,只需要更改__proto__指向的对象即可(可能是原型继承唯一的优点)。任何对象都有原型属性,且这个原型属性可以追本溯源到object对象,包括object对象自身。

4.4.2 类继承

类继承也是ES6新增的方法。在ES6之前,通过下面的方法创建一个学生类:

1
2
3
4
5
6
7
8
9
// 创建一个学生类(指定一个函数当作构造函数)
function Student(name) {
this.name = name;
}
Student.prototype.sayName = function() {
console.log('My name is ' + this.name);
}
let s = new Student("Debussy"); // s 是一个对象
s.sayName();

在ES6之后才可以使用class关键字:

1
2
3
4
5
6
7
8
9
10
11
12
class Student {
constructor(name) {
this.name = name;
}

sayName() {
console.log('My name is ' + this.name);
}
}

let s = new Student("Debussy");
s.sayName();

使用下面的方法继承:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Student {
constructor(name) {
this.name = name;
}

sayName() {
console.log('My name is ' + this.name);
}
}

class PrimaryStudent extends Student {
constructor(name, grade) {
super(name); // 调用父类的构造函数
this.grade = grade;
}

sayGrade() {
console.log('I am in grade ' + this.grade);
}
}

let ps = new PrimaryStudent("Debussy", 1);
ps.sayName();
ps.sayGrade();

简单的回顾一下构造函数、原型和实例的关系:每个构造函数都有一个原型对象,原型对象都包含一个指向构造函数的指针,而实例都包含一个指向原型对象的内部指针。那么假如我们让原型对象等于另一个类型的实例,结果会怎样?显然,此时的原型对象将包含一个指向另一个原型的指针,相应地,另一个原型中也包含着一个指向另一个构造函数的指针。假如另一个原型又是另一个类型的实例,那么上述关系依然成立。如此层层递进,就构成了实例与原型的链条。这就是所谓的原型链的基本概念。

——摘自《javascript高级程序设计》

image-20240503205317107

5 内部对象

JavaScript中万物皆对象。常见的对象类型有如下几种(可以用typeof关键字获取)

5.1 Date对象

可以使用Date获取当前时间和计算时间:

1
2
3
4
5
6
7
8
9
10
11
12
let now = new Date();

console.log(now);
console.log(now.getFullYear()); // 年份
console.log(now.getMonth()); // 月份,1月为0
console.log(now.getDate()); // 日期
console.log(now.getDay()); // 星期几,0为星期天
console.log(now.getHours()); // 小时
console.log(now.getMinutes()); // 分钟
console.log(now.getSeconds()); // 秒
console.log(now.getMilliseconds()); // 毫秒
console.log(now.getTime()); // 距1970年1月1日0时0分0秒的毫秒数(时间戳)

可以使用时间戳创建时间:

1
2
3
4
5
6
7
8
let time = new Date(1714713565465);

console.log(time);
console.log(time.toLocaleString()); // 2024/5/3 13:19:25
console.log(time.toLocaleDateString()); // 2024/5/3
console.log(time.toLocaleTimeString()); // 13:19:25

console.log(time.toGMTString()); // Fri, 03 May 2024 05:19:25 GMT

5.2 JSON对象

JSON是一种数据格式,用于传输特定数据,本质上就是一串字符串。JavaScript中一切皆为对象,任何JavaScript支持的类型都可以用JSON来表示。

1
2
3
4
5
6
7
8
9
10
11
12
13
let Debussy = {
name: "Debussy",
age: 20,
isMarried: false,
skills: ["HTML", "CSS", "JS"],
greet: function() {
console.log("Hello, my name is " + this.name);
}
}

let jsonDebussy = JSON.stringify(Debussy); // 返回一个字符串,函数会被省略'{"name":"Debussy","age":20,"isMarried":false,"skills":["HTML","CSS","JS"]}'

let parsedDebussy = JSON.parse(jsonDebussy);// 返回一个对象

6 BOM和DOM

6.1 BOM对象

在Web开发中,常用B(Browser)代表浏览器,S(Server)代表服务端。BOM全称为Browser Object Module,即浏览器对象模型。

6.1.1 window(重要)

window对象可以有两层含义:一可以指浏览器窗口,二是可以指唯一的全局作用域。window有下面的一些常用的方法和属性:

1
2
3
4
5
6
7
window.alert('Hello');
window.console.log('World');

console.log(window.innerHeight);
console.log(window.innerWidth);
console.log(window.outerHeight);
console.log(window.outerWidth);
image-20240504002359321

navigator封装了浏览器的信息,它是window的一个属性。它比较常用的用法是获取当前的浏览器信息和用户信息,比如可以获知当前的网页打开使用的是什么浏览器,是手机浏览器还是PC浏览器,浏览器内核是什么,PC的OS信息等:

image-20240504003301819

大多数时候不会使用navigator对象,因为它可以被人为修改。不建议使用这些属性来判断和编写代码。

6.1.3 screen

代表屏幕信息。浏览器的功能远比想象的强大,它甚至可以操作浏览器之外的很多部件,获取很多信息。

image-20240504003632278

6.1.4 location(重要)

location可以获取当前页面的URL信息。

1
2
3
4
5
6
7
console.log(location.host);     // 主机 127.0.0.1:5500
console.log(location.href); // http://127.0.0.1:5500/test.html
console.log(location.pathname); // /test.html

// location.reload(); // 刷新网页

location.assign("https://www.baidu.com"); // 跳转到百度(将该网站连接到一个新地址)

6.1.5 document

document代表当前的页面的html文件中的DOM文档树。可以通过document获取当前浏览器中文档树的节点。能获取就能做到增删改查。

image-20240504005547447
1
2
3
4
5
6
7
8
9
10
11
<dl id="abc">
<dt>Debussy</dt>
<dd>Debussy is learning JavaScript</dd>
<dd>Debussy must learning FPGA</dd>
</dl>
<script>
// "use strict";

let dl = document.getElementById('abc');
console.log(dl);
</script>
image-20240504005954901

document还可以获取cookie。cookie包含了客户端的一些本地信息,通过这些信息,可能直接通过劫持这些cookie来非法登录网页,非法获取隐私等。服务器端可以设置cookie为httpOnly,就可以规避这个问题,提高网页使用安全性。

6.1.6 history

代表浏览器的历史记录,通过这个对象可以前进和后退,但是不建议使用这种方法。浏览器网页的跳转还是建议在服务端实现。

1
2
history.forward();
history.back();

6.2 DOM对象

DOM全称Document Object Module,即文件对象模型。整个浏览器网页就是一个树形结构。通过操作DOM,可以实现网页元素的行为。要操作一个Dom节点,就必须先获得这个Dom节点

一个标签就是一个节点,在网页上就体现为一个元素。

6.2.1 获取Dom节点

首先介绍(复习)一些常见的Dom节点及其作用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<div id="father">
<dl>
<h1 class="title">This is a h1 node</h1>
<dt>This is a dt node</dt>
<dd class="description">Lorem ipsum dolor sit amet consectetur adipisicing elit. Omnis laborum quia quos
sint repellat aut!
Fugiat et soluta exercitationem omnis rerum doloremque, doloribus, beatae culpa asperiores non minus
temporibus suscipit.</dd>
</dl>

<p>Lorem ipsum dolor, sit amet consectetur adipisicing elit. Perferendis dolorem nostrum, aliquid aspernatur
cumque at illum, accusamus reprehenderit eaque fugit reiciendis itaque. At enim vero maiores eius laboriosam
sunt natus.</p>
</div>
<script>
// "use strict";

let h1 = document.getElementsByTagName("h1"); // 通过标签名获取元素
let dd = document.getElementsByClassName("description"); // 通过类名获取元素
let div_father = document.getElementById("father"); // 通过id获取元素

let div_children = div_father.children; // 获取父元素下的所有子元素
let div_child_1 = div_father.firstElementChild; // 获得第一个子元素 dl
let div_child_2 = div_father.lastElementChild; // 获得最后一个子元素 p

</script>

上面的代码是原生代码,后续基本不使用。之后一般使用jQuery()获取Dom节点,但是了解原生写法还是很有必要的。

About this Post

This post is written by Yun Zhang, licensed under CC BY-NC 4.0.

#JavaScript