JavaScript基础

组成

js是基于ECMAScript的语言层面的实现

1.ECMAScript 定义语言规范

2.DOM 用于操作文档API

3.BOM 用于操作浏览器的API

最终用JavaScript引擎实现

基本语法

编写位置

1、编写在html内部(不推荐)

1
2
<a href="#" onclick="alert('百度一下')">百度一下</a>
<!-- alert为一个Js函数 -->

2.编写在script元素(html代码)之内

1
2
3
<script>
JS函数
</script>

3. 独立的js文件

直接建立一个.js文件 然后在html中进行引用即可

1
<script src="js文件相对位置"></script>

< noscript >元素

此元素被用于给不支持js的浏览器提供替换内容

1
2
3
<noscript>
<h1>不支持滴滴滴</h1>
</noscript>

注意当浏览器支持js的时候 noscript元素是不会被显示的

注意事项

1.script标签 不能写成单标签 且在外部引用js文件的时候 标签内 不能再写js代码

2.script 省略type属性

3.js默认作为html加载顺序 即 自上而下加载 故而推荐 把script元素和js代码放在body内的最后一部分(子元素最后一行)

4.js代码 严格区分大小写

5.js中把换行符当作 隐式分号 (自动插入分号)

交互方式

交互方法 方法说明 效果查看
alert (函数) 接受一个参数 弹窗查看
console.log (方法) 接受多个参数 在浏览器控制台(console)查看 (使用最多)
document.write 接受多个字符串 在浏览器页面查看
prompt 接受一个参数 在浏览器接受用户输入

文本注释

单行注释和多行注释跟cpp完全相同 注意不支持嵌套注释 略去 在这里介绍文本注释

1
2
3
4
5
6
7
8
9
10
11
12
/** + tab
第一行用于注解整个函数
后面每一行用于注解函数的各个参数

/**
* 打招呼的函数
* @param {string} name 姓名
*/
function sayHello(name) {
console.log(`Hello ${name}`);
}

变量和数据类型

变量的声明-> var关键字 (后续还有let const声明方式)

特别需要注意的是 var 声明的变量存在函数作用域和全局作用域,却没有块级作用域。

1
var 变量名称 = 赋值

简单来说 除了要用var关键字之外 其他书写方式 命名规则跟cpp一模一样

规范与注意事项

规范

1.多个单词 使用 驼峰命名法 currentTime

小驼峰(currentTime 首个首字母小写 其余首字母大写)

大驼峰(CurrentTime 每个首字母都大写)

注意事项

  1. 与cpp一样 变量不能不声明直接使用 但不同的地方在于 可能在window中可能存在一些变量 所以可能会输出一些东西

  2. 如果一个变量声明后没有赋值操作 值为undefined

  3. 不用var也可以声明一个变量 并且这个变量会加入window对象

  4. 与cpp的区别是 js的数据类型是动态的(动态类型) 即它的类型可以一直变化 类型是非常灵活的

    类型系统是松散的

    1
    2
    var name = "why";
    name = 3;

数据类型

在JavaScript中有8种数据类型(7种原始类型和1种复杂类型)

1.Number 2.String

3.Boolean 4.Undefined (只有und一个值)

5.Null 6.Object (复杂类型)

7.Biglnt 8.Symbol

1.Number类型

注意Number类型不区分整数和浮点数

进制相关表示与之前cpp学的完全相同

数字表示范围
1
2
var a = Number.MAX_VALUE;  //最大正数值
var b = Number.MIN_VALUE; //最小正数值 小于这个数会被转化为0
特殊数值

Infinity 无穷

NaN (not a number) 即运算结果不是数字 比如字符串与数字相加

isNaN

用于判断是不是一个数字 不是数字就返回true 是数字就返回false

1
2
console.log(isNaN(a));
//会输出布尔量true or false
2.String类型

注意字符串一旦定义后是不可改变的(可以被重新赋值) 即不能改变字符串内部的东西 比如不能像cpp中那样 s[0] = ‘a’ 是不支持的

引号使用

字符串必须被括在引号内 在JavaScript中支持三种引号的使用

1
2
3
var s1 ="Hello";
var s2 ='World';
var s3 = `I want to ${s1} ${s2}`;

即单引号双引号和反引号··(tap上面那个)

反引号是在ES6中的特殊语法 其可以使用$来在字符串中添加其他的变量或者表达式

1
2
3
4
5
var s3 = `I want to ${s1} ${s2}`;
console.log(s3);
//会输出 I want to Hello World
var s3 = `I want to ${s1} ${s2} ${2+3}` ;
//还可以添加表达式 会直接输出该表达式的值
转义符
转义字符 表示符号
\' 单引号
\" 双引号
\\ 反斜杠
\n 换行符
\r 回车符
\t 制表符
\b 退格符
字符串本身的方法和属性

长度属性

1
2
console.log(s1.length)
//不一样的是不需要括号 因为这是属性

字符串拼接

字符串拼接的方法

1
2
3
4
5
var name = "Jimmy";
var k = "my name is ";
var re = k + name; //用+运算符连接
var re2 = `${k}${name}`; // 比较推荐的方法
//此时获得的re 和 re2字符串完全相同
3.Boolean类型

即cpp中的bool类型 同样用true 和 false来表示

其他基本数据类型在初始化(这里的初始化指的是初始化为0 空字符串这些)后进行逻辑判断时 会自动转化为false

1
2
3
4
var name=""
if(name){
console.log("true")
}

这样就不会输出true 因为此时转化为了false

4.Undefined类型

只有一个值即undefined

当变量没有被赋值的时候 其值就是undefined

但注意不要在定义变量的时候初始化为undefined

比如在定义字符串的时候初始化为“”空字符串 null空对象

5.Object类型 Null类型

引用类型或复杂类型

其他原始类型只保存一个单独的内容

而Object可以表示一组数据 是其他数据的一个集合

用{}来表示一个对象 即创建一个Object(对象)

1
2
3
4
5
var person = {
name:"Jimmy",
age:18,
sex:"male",
}

其实就跟cpp里的结构体非常像了 比如说我们需要访问这个对象中的某一个元素时同样是使用 对象名.属性名

1
console.log(person.name)
初始化

如果用{}来初始化 依然会在堆内存中创建一个对象 所以当用if判断的时候其的bool类型其实是true

而如果用null来初始化的话就不会创建这个对象 并且会转化为false

当对一个对象进行初始化的时候 不建议初始化为{} 建议初始化为null

可以觉得Null类型存在的意义就是给对象来初始化

数据类型转换
1.隐式转换

字符串类型转化

一 ,如果 + 操作符左右 有一个是字符串 那么另一边会自动转换为字符串类型进行拼接

1
2
3
4
var a = 1;
var aString = a + "";
console.log(typeof aString)
//此时aString已经是字符串了

二,console.log函数是默认转换为字符串然后输出的

Number类型转换

三,当对两个变量进行非加的运算符操作的时候 会转化为Number类型

1
2
3
4
var b = "5"
var c = "6"
var d = b * c;
console.log(d);

此时d会输出30

Boolean类型转换

当进行逻辑运算的时候会直接隐式转换成Boolean类型

1
2
3
4
5
6
7
var a = 1;
if(a){
console.log("true")
}
//直观上为“空“ 的值 (即正确初始化 0 null等) 将转化为false
//其他值变为true
//非空字符串始终为true
2.显式转换

String类型转换

调用String()函数

显示转化为字符串类型

1
2
var a = 2;
var aString2 = String(a);

调用 toString() 方法 (后续介绍)

Number类型转换

调用 Number函数

显示转化为Number类型

1
2
var a = "2";
var aNumber = Number(a);
转换后的值
undefined NaN
null 0
true 和 false 1 and 0
string 去掉首尾空格后的纯数字字符串中含有的数字。如果剩余字符串为空,则转换结果为 0。否则,将会从剩余字符串中 “读取” 数字。当类型转换出现 error 时返回 NaN。

Boolean类型转换

1
2
3
4
console.log(Boolean(a))
//直观上为“空“ 的值 (即正确初始化 0 null等) 将转化为false
//其他值变为true
//注意包含0的字符串是true 即非空字符串始终为true

typeof操作符

1
2
3
4
<script>
var a = "mine";
console.log(typeof a);
</script>

由于ECMAScript的类型系统是松散的(动态类型)所以需要用typeof操作符来确定任意变量的数据类型

  • 对一个值使用typeof

    操作符会返回下列字符串之一:

    • "undefined" 表示值未定义;
    • "boolean" 表示值为布尔值;
    • "string" 表示值为字符串;
    • "number" 表示值为数值;
    • "object" 表示值为对象 (而不是函数) 或 null
    • "function" 表示值为函数;
    • "symbol" 表示值为符号;

注意 当看到 typeof () 的时候也不要认为这是一种函数 其实

1
typeof (x) 与 typeof x 完全等效

用括号只是表明typeof操作的是后面一个整体(把括号内的东西当作一个整体)而已

基础运算符

其基本定义 包括运算符和运算元 和cpp中的概念都是完全相同的 故而只在这里的笔记中记录跟cpp语法中不同的部分

幂运算

运算符 运算规则 范例 结果
** 幂 (ES7) 2 ** 3 8

== 和 ===

==运算符会先把两边的变量转换为Number类型的值 即会存在一个隐式转换 故而存在一个问题即无法区分0和空字符串 false等(因为它们转换为Number类型都为0)(注意null不行 因为它被当成一个对象!)

默认情况下 null == undefined

==运算符会导致类型不相同的变量可能会被记为相等 所以不常用

**严格相等运算符===**不会进行隐式转换 故而对于类型不同的元素会直接返回false

分支语句

ifelse语句 三元运算符 都跟cpp完全一致

switch语法也几乎与cpp中完全相同 至少需要一个case代码和一个可选的default代码块

for循环

for循环的基本使用方法也基本相同 但特别需要注意的是 由于var声明的变量是全局变量 所以for循环中的索引var i 在其中调用的时候永远都会是i 的最终值

逻辑运算符

注:用于条件判断时的逻辑运算符使用方法是跟cpp完全相同的

或 ||

或的本质

||(或) 两个竖线符号表示 “或” 运算符(也称为短路或)

1
result = a || b;
  • 从左到右依次计算操作数。
  • 处理每一个操作数时,都将其转化为布尔值(Boolean)。
  • 如果结果是 true,就停止计算,返回这个操作数的初始值。
  • 如果所有的操作数都被计算过(也就是,转换结果都是 false),则返回最后一个操作数。

本质推导

用来获取第一个有值的结果 从而保证操作的安全性

1
2
3
jvar info = undefined
var message = info || "我是默认值"
console.log(message.length)

常见于函数需要传入参数的时候 为了防止传入参数不正确而导致操作错误

与 &&

与的本质

(短路与)

  1. 拿到第一个运算元,将运算元转成 Boolean 类型
  2. 对运算元的 Boolean 类型进行判断
    • 如果 false,返回运算元 (原始值)
    • 如果 true,查找下一个继续来运算
    • 以此类推
  3. 如果查找了所有的都为 true,那么返回最后一个运算元 (原始值)

本质推导

对一些对象中的方法进行有值判断

1
2
3
4
5
6
7
8
9
10
var obj = {
name: "man",
friend: {
name:"kobe",
eating: function(){
console.log("eating")
}
}
}
obj&&obj.friend&&obj.friend.eating&&obj.friend.eating()

这样可以保证不会对空对象进行调用

非 !

两个非!!有时候可以用来将变量转换为boolean类型

原理:1、第一个非 将原变量取反 转换为了boolean类型

2、第二个非 再次取反 即能获得原变量的boolean值

全局对象object - > window

1.查找变量时最终都会找到window的头上

2.一些浏览器全局提供给我们变量 函数 对象 都会放在window对象上面

3.使用var定义的变量都会被默认添加到window上面

->不管var在哪写 任何时候打印window都会出现这个var变量的

函数

函数也是一种对象 可以往里面添加属性

函数的定义(函数的声明)

使用 function关键字

1
2
3
function 函数名(需要传入的参数) {
函数封装的代码
}

跟cpp还是比较相似的 只是关键词只为function 形参直接写变量名即可

函数的调用直接通过函数名()即可

1
函数名()
1
2
3
4
function sayHello(word){
consol e.log(`I want to say ${word}`)
}
sayHello("NJYgocrazy")

会输出I want to say NJYgocrazy

与cpp略有不同的是 在js中 函数中还可以写其他函数 并且调用其里面的函数

1
2
3
4
5
6
7
8
9
10
11
function sayHello(word,name,age){
console.log(arguments[0])
console.log(arguments[1])
for(var i = 0;i < arguments.length;i++) {
console.log(arguments[i])
}
function hi(){
console.log("ahah")
}
hi();
}

函数的返回值

与cpp类似 同样使用return关键字来返回结果 并且return的部分也会立即停止执行 可以返回变量或具体值

不同的是 js里不存在void类型 如果不写return 或者 return后不接任何内容的话 返回值即为 undefined

arguments参数(高级中学习)

函数传入的参数都默认放在arguments中 其是一个类数组object类型 和数组的使用方法很相似

该对象存放所有调用者传入的参数 索引从0开始依次存放

即使在函数定义中没有对应的形参与传入的形参对应 也会放进arguments中 并且可以通过它去访问

访问方法跟数组很像 都是用数组索引访问 or 遍历访问

1
2
3
4
5
6
7
8
function sayHello(word,name,age){
console.log(arguments[0])
console.log(arguments[1])
for(var i = 0;i < arguments.length;i++) {
console.log(arguments[i])
}
}
sayHello("NJYgocrazy","liln","18","more")

此时会输出NJYgocrazy liln NJYgocrazy liln 18 more

递归(略)

与cpp一样 js中的函数同样可以进行 递归操作

参数访问范围

在js中 函数优先访问自己函数内的变量 如果找不到 再访问函数外的全局变量

函数表达式

注意在js中 函数并非一种语法结构 而是一种特殊的值 进一步的 其是一种特殊的对象 故而不论怎么定义 其都是一种值

函数定义除了上面的函数声明之外还可以通过函数表达式来定义函数

1
2
3
4
var foo = function(){
console.log("foo")
}
foo()

注意:函数表达式定义时 function关键字后面没有函数名 函数名使用的是前面的变量名

函数表达式和函数声明的区别

函数表达式来定义函数的时候 函数仅仅在程序运行到此处后才能进行

用函数声明来定义函数的时候 函数可以在定义之前就被调用

更推荐使用函数声明来定义函数

头等函数

头等函数指的是在程序设计语言中 函数被当作头等公民 而js就符合头等函数这种编程设计

符合头等函数编程方式就称为 函数式编程 而JavaScript就是符合的语言

即是指 函数可以作为别的函数的参数 作为函数的返回值 赋值给其他变量 储存在数据结构中

其实它们的原理都是 函数是一种值 故而可以在变量之间进行赋值 这是跟cpp的一大区别 即变量的类型是多样的

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
27
28
29
30
31
32
//1.赋值给变量
var foo = function(){
console.log("foo")
}
foo()
//2.函数在变量之间进行传递
var foo1 = foo
foo1()

//3.作为参数
function bar(fn){
console.log("fn:",fn)
fn()
}
bar(foo)
//注意这里不能加小括号 否则会调用执行foo函数 相当于传入的undefined

//4.作为返回值
function bar1(){
return foo
}
var re = bar1()
console.log(re)

//5.作为对象属性
var obj = {
foo:foo
}
obj.foo()

//6.储存在数据结构中
var fns = [foo,sayHello]

回调函数 匿名函数

函数回调的概念理解

在 JavaScript 里,回调函数是当作参数传递给另一个函数的函数,并且会在那个函数内部被调用执行。回调函数能够让代码以异步方式运行,还能实现代码的复用与模块化。

回调函数的特点

  • 作为参数传递:回调函数可作为参数传递给其他函数,这样就能把具体操作逻辑的控制权交给调用者。
  • 异步执行:回调函数常被用于异步操作,像事件处理、定时器和网络请求等,从而避免阻塞主线程。
  • 延迟执行:回调函数并非马上执行,而是在某个特定条件达成或者某个操作完成之后才会执行。

回调函数的使用场景

  • 事件处理:在浏览器中,事件处理函数就是回调函数,在特定事件发生时执行。
  • 定时器setTimeoutsetInterval 函数接受回调函数作为参数,在指定时间后或每隔一段时间执行。
  • 网络请求:在进行 AJAX 请求时,通常会使用回调函数来处理响应结果。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 定义一个函数,接受回调函数作为参数
function greet(name, callback) {
console.log(`Hello, ${name}!`);
// 调用回调函数
callback();
}

// 定义回调函数
function sayGoodbye() {
console.log('Goodbye!');
}

// 调用 greet 函数,并传入回调函数
greet('John', sayGoodbye);

在这个例子中,sayGoodbye 函数作为回调函数传递给了 greet 函数,在 greet 函数内部调用了 callback(),也就是执行了 sayGoodbye 函数。

从写法上来讲 回调函数可以简单描述为通过一个函数来执行作为参数传入的函数

这里还可以使用 匿名函数 来对上述函数进行简化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 定义一个函数,接受回调函数作为参数
function greet(name, callback) {
console.log(`Hello, ${name}!`);
// 调用回调函数
callback();
}

// 定义回调函数
// function sayGoodbye() {
// console.log('Goodbye!');
// }

// 调用 greet 函数,并传入回调函数
greet('John', function(){console.log('Goodbye!')})

此时可以注意到 当调用greet函数的时候 其中的function()是没有函数名的 这就是匿名函数 (注意这里匿名函数的定义是放在调用greet函数的括号内的!)

此时像greet这样的函数可以被称之为 高阶函数

其必须至少满足两个条件

1.接受一个或多个函数作为输入

2.输出一个函数

立即执行函数

顾名思义 就是说一个函数定义完后被立即执行

1.定义了一个匿名函数 这个函数有自己独立的作用域

2.后面的() 表示这个函数被执行了

1
2
3
4
//这里用小括号表示把包裹内容当作一个整体
(function(参数){
函数内部代码
})(传入的参数)

立即执行函数其他部分跟普通函数的性质都是一样的 包括头等函数性质 包括可以有返回值

由于立即执行代码有其独立的作用域 所以可以防止全局变量冲突

可以避免外界访问或者修改内部的变量 同时避免了对内部变量的修改

1
2
3
4
5
6
7
var fns = [1,2,3,4,5]
for(var i = 0; i < fns.length; i++){
var num = fns[i];
(function fn(){
console.log(`${num}是第${i}个数字`)
})()
}

得益于立即执行函数单独作用域的特点 相当于在每次循环中都创建了一个单独的函数作用域 所以每次fn调用的for都是与当前循环时候的i索引绑定的 而不会因为i是var全局变量的原因导致每次的i都是最终的更新值

面向对象

对象类型用来储存键值对 其实就很像结构体的内容了

  • 这个时候,我们需要一种新的类型将这些特性和行为组织在一起,这种类型就是对象类型。
    • 对象类型可以**使用{...}**来创建的复杂类型,里面包含的是**键值对(“key: value”)**;
    • 键值对可以是属性和方法(在对象中的函数称之为方法)
    • 其中key 是字符串(也叫做属性名 property name ,ES6 之后也可以是 Symbol 类型,后续学习) 作为key值的时候只要是单个单词字符串的引号是可以省略的;
    • 其中value 可以是任意类型,包括基本数据类型、函数类型、对象类型等;
1
2
3
4
5
6
7
8
var person = {
name: '张三',
age: 18,
sex: '男',
show: function(){
console.log(`姓名:${this.name},年龄:${this.age},性别:${this.sex}`)
}
}

这里跟java中的概念挺像的 像show这样放在对象中的函数类型我们称之为方法 即

一个函数放到对象中作为一个 属性 那么这个函数称之为方法

对象的创建和使用

  • 对象的创建方法有很多,包括三种:
    • 对象字面量(Object Literal):通过{}
    • new Object + 动态添加属性;
    • new 其他类;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var person = {
name: '张三',
age: 18,
sex: '男',
show: function(){
console.log(`姓名:${this.name},年龄:${this.age},性别:${this.sex}`)
}
}


var obj2 = new Object();
obj2.name = "n";

function person(){}
var obj3 = new person();

其实第二三种方法就很像java中创建对象的方法了

对象属性的访问跟cpp的结构体是一致的 使用点.关键字来调用

1
person.name

对象属性的修改只需要拿到然后重新赋值即可

1
person.name = "james"

对象属性的添加直接把新的属性写上即可 如果没有就添加 有的话就是修改 体现了JavaScript的灵活

1
2
//比如这里我想额外添加一个height属性
person.height = "1.8m"

对象属性的删除 需要用到 delete关键字(操作符)

1
delete person.age

对象中的方括号调用

由于键值对规定 不能含有空格或其他特殊符号 不能以数字开头

所以可以用方括号来使得变量的定义更加灵活

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var person = {
name: '张三',
age: 18,
sex: '男',
"my friend": "李四",
show: function(){
console.log(`姓名:${this.name},年龄:${this.age},性别:${this.sex}`)
},
"eating something": function(){
console.log("eating")
}
}
var myf = person["my friend"]
var eatkey = "eating something"
person[eatkey]()

对象的遍历

表示获取对象中所有的属性和方法

1
object.keys()

会返回一个由一个给定对象的自身可枚举属性组成的数组

1
2
3
4
5
6
7
8
9
10
//for循环遍历方法一
var personkeys = Object.keys(person)
for(var i = 0; i < personkeys.length; i++){
var key = personkeys[i];
console.log(person[key])
}
//for循环遍历方法二 类似于cpp的加强for循环
for(var key in person){
console.log(person[key])
}

注意 因为每次得到的属性名字key是不确定的 所以这里都使用中括号获取value

this

函数中有一个this变量 大多数情况下会指向一个变量

指向谁只跟如何被调用有关

this的指向

如果一个普通的函数被 默认调用 那么this指向的就是window

1
2
3
4
function sayHello(word){
console.log(word)
}
sayHello("hello")

如果函数式被某一个对象来引用并且调用它的话 那么这个this就会指向这个对象(调用的对象)

1
2
3
4
5
6
7
8
var obj = {
running:function(){
console.log("running")
console.log(this === obj) //true
}
}

obj.running()

this的作用

可以灵活地访问当前调用对象的属性 不用外界干预

1
2
3
4
5
6
7
8
var obj = {
name: "nn",
running:function(){
console.log(`${this.name}is running`)
}
}

obj.running()

比如这样 无论这个obj的名称怎样改变 每次输出的都会是obj的名字 因为this始终指向的都是obj

创建一系列对象

1、工厂函数 - > 一种设计模式

1
2
3
4
5
6
7
8
function createStudeent(name, age) { 
var stu = {}
stu.name = name
stu.age = age
return stu
}
var stu1 = createStudeent('张三', 18)
var stu2 = createStudeent('李四', 19)

2.构造函数(类)

使用 new关键字来创建构造函数

如果一个普通的函数被使用new操作符来调用了 那么这个函数就称之为是一个 构造函数

构造函数上添加的函数称之为 类方法

如果一个函数被使用new操作符调用了,那么它会执行如下操作:

  1. 在内存中创建一个新的对象(空对象);
  2. 这个对象内部的[[prototype]]属性会被赋值为该构造函数的prototype属性;(后面详细讲);
  3. 构造函数内部的this,会指向创建出来的新对象;
  4. 执行函数的内部代码(函数体代码);
  5. 如果构造函数没有返回非空对象,则返回创建出来的新对象;

相当于用new 会自动帮你创建一个对象 就不用像工厂函数那样每次在函数中手动创建了

1
2
3
4
5
6
function createStudent(name, age) { 
this.name = name;
this.age = age;
}
var stu1 = new createStudent('张三', 18);
console.log(stu1)

构造函数与工厂函数之间的区别

工厂函数创建的对象如果打印的话会发现创建的对象还是Object的默认类型而不是我们想要创建的特定对象

而构造函数创建的对象 打印时它们的对象类型都是构造函数的函数名

在JavaScript中 构造函数其实扮演了其他语言中类的角色

在ES5之前 还是用function来声明一个构造函数(为了区分 创建的时候可以首字母大写或使用大驼峰命名) 然后用new关键字来进行调用

在ES6之后 JavaScript可以像其他语言一样通过class来声明一个类

构造函数与类

在JavaScript中构造函数其实就是其他语言中的类

构造函数就类似于一个描述和一个模板 通过它来创建一个实体对象

在这个描述过程中是不会创建对象的

其实跟java类似的是 JavaScript中的object也是一个自带的类

平时创建普通对象的时候下面两种写法其实是完全等效的

1
2
var a = {}
var a = new object()

栈内存和堆内存

  • 我们知道程序是需要加载到内存中来执行的,我们可以将内存划分为两个区域: 栈内存和堆内存

    • 原始类型占据的空间是在栈内存中分配的;

      原始类型在变量中保存的是值本身 故而原始类型也称为 值类型

    • 对象类型占据的空间是在堆内存中分配的;

      对象类型在变量中保存的是对象的引用(因为对象类型属性的值会储存在堆内存中 而对象变量本身是放在栈内存中的 所以只储存对象类型占据空间的地址)故而对象类型也被称为引用类型

image-20250508194615324

1
2
3
4
5
6
7
8
9
10
var obj = {
foo: "hahah",
bar: 123
}

var info = obj

var name = "why"

var nickname = name

概念

在JavaScript中 构造函数就是类

使用 new关键字来创建构造函数

如果一个普通的函数被使用new操作符来调用了 那么这个函数就称之为是一个 构造函数

构造函数上添加的函数称之为 类方法

如果一个函数被使用new操作符调用了,那么它会执行如下操作:

  1. 在内存中创建一个新的对象(空对象);
  2. 这个对象内部的[[prototype]]属性会被赋值为该构造函数的prototype属性;(后面详细讲);
  3. 构造函数内部的this,会指向创建出来的新对象;
  4. 执行函数的内部代码(函数体代码);
  5. 如果构造函数没有返回非空对象,则返回创建出来的新对象;

在JavaScript中 构造函数其实扮演了其他语言中类的角色

在ES5之前 还是用function来声明一个构造函数(为了区分 创建的时候可以首字母大写或使用大驼峰命名) 然后用new关键字来进行调用

在ES6之后 JavaScript可以像其他语言一样通过class来声明一个类

构造函数与类

在JavaScript中构造函数其实就是其他语言中的类

构造函数就类似于一个描述和一个模板 通过它来创建一个实体对象

在这个描述过程中是不会创建对象的

原始类型的包装类

注意 null undefined没有任何的方法或者属性 也没有对应的对象包装类

在实际应用中其实可以发现 虽然JavaScript中的原始类型并非对象类型 但实际上也是可以调用其方法或者属性的 比如说

1
2
3
var name = "nn"
var length = name.length
console.log(length)

这是因为JavaScript对原始类型封装了对应的包装类型(在这一点上是和java非常相似的 都是String Number这样首字母大写的形式)

相当于会自动创建一个类型

1
2
3
4
5
6
var name = "nn"
// name = new String(name) 转换为对象就可以使用方法和属性了
var length = name.length
console.log(length)

//获取完后会再将这个对象销毁掉

默认情况,当我们调用一个原始类型的属性或者方法时,会进行如下操作:

  • 根据原始值,创建一个原始类型对应的包装类型对象;
  • 调用对应的属性或者方法,返回一个新的值;
  • 创建的包装类对象被销毁;
  • 通常 JavaScript 引擎会进行很多的优化,它可以跳过创建包装类的过程在内部直接完成属性的获取或者方法的调用。

Number类的补充

Number 属性补充

  • Number.MAX_SAFE_INTEGER:JavaScript 中最大的安全整数 (2^53 - 1);

  • Number.MIN_SAFE_INTEGER:JavaScript 中最小的安全整数 (-(2^53 - 1)

  • 数字表示范围

    1
    2
    var a = Number.MAX_VALUE;  //最大正数值
    var b = Number.MIN_VALUE; //最小正数值 小于这个数会被转化为0

Number 实例方法补充

  • 方法一

    1
    toString(base)

    将数字转成字符串,并且按照base进制进行转化

    • base 的范围可以从 2 到 36,默认情况下是 10;
    • 注意:如果是直接对一个数字操作,需要使用..运算符;
  • 方法二

    1
    toFixed(digits)

    ,格式化一个数字,保留digits位的小数;

    • digits的范围是 0 到 20(包含)之间;

      返回string类型

Number 类方法补充

  • 方法一Number.parseInt(string[, radix]),将字符串解析成整数,也有对应的全局方法parseInt
  • 方法二Number.parseFloat(string),将字符串解析成浮点数,也有对应的全局方法parseFloat

Math对象

内置对象(不是构造函数 所以不能new) 其中包含一些数学方面的属性和函数方法

Math 常见的属性:

  • Math.PI:圆周率,约等于 3.14159;

Math 常见的方法:

  • Math.floor:向下舍入取整

  • Math.ceil:向上舍入取整

  • Math.round:四舍五入取整

  • Math.random:生成 0~1 的随机数(包含 0,不包含 1)当然这个乘上其他数就可以生成预想范围的随机数了

  • Math.pow(x, y):返回 x 的 y 次幂

  • Math.abs(x):返回x的绝对值

String类型的补充

String 常见的属性:

  • length:获取字符串的长度;

操作一:访问字符串的字符

  • 使用方法一:通过字符串的索引 str[0]
  • 使用方法二:通过str.charAt(pos)方法
  • 它们的区别是索引的方式没有找到会返回undefined,而charAt没有找到会返回空字符串;

操作二:字符串的遍历

  • 方式一:普通for循环
  • 方式二:for..of遍历 (String函数内部让它变成了一个可迭代对象 这里补充一下 目前可迭代对象只有字符串和数组)
1
2
3
4
5
6
7
8
9
var str = "hello world"
console.log(str[0])
console.log(str.charAt(0))
for(var i = 0;i < str.length;i++) {
console.log(str[i])
}
for(var i of str) {
console.log(i)
}

操作三:对字符串的修改

之前提到过字符串一旦被定义是不能从内部改变的 所以下面的操作并不是改变了字符串 而是重新返回了一个新的字符串

  • 所以,在我们改变很多字符串的操作中,都是生成了一个新的字符串;
    • 比如改变字符串大小写的两个方法
      • toLowerCase():将所有的字符转成小写;
      • toUpperCase() :将所有的字符转成大写;
1
2
3
4
var str1 = str.toLowerCase()
var str2 = str.toUpperCase()
console.log(str1)
console.log(str2)

操作四:查找字符串

判断一个字符串中是否含有另一个字符串

方法一:

indexof方法

1
indexof(要搜索的字符串,从哪个位置开始搜)
1
2
3
4
5
6
7
8
9
var str = "I want to see the world"
var findstr = "world"
var index = str.indexOf(findstr) //所寻找字符串的第一个字母所在位置
if(index !== -1) {
console.log(index)
}
else {
console.log("Not found") //找不到时会返回-1
}

方法二:

includes方法

ES6中新增了一个方法 用来判断包含关系

1
2
3
if(str.includes(findstr)){
console.log("Found")
}

直接返回的是布尔值 true / false

方法三

startsWith方法 是否以xxx开头

1
2
str.startsWith(string,position)
//表示从position位置开始 判断字符串是否以string开头

**endsWith方法 是否以xxx结尾 **

1
2
str.endWith(string,length)
//表示在length长度内 判断字符串是否以string结尾

操作五:替换字符串

同样地 是返回一个新的字符串 先找到目标字符串再替换成新字符串 使用 replace关键字

1
2
3
4
var newstr = str.replace("world","earth")
//replace(查找字符串,替换字符串)
//这里可以传入一个正则表达式来查找 也可以传入一个函数来替换(可以使用匿名函数)
console.log(newstr)

操作五:获取子字符串

方法 选择方式 负值参数
slice(start, end) startend(不含 end 允许
substring(start, end) startend(不含 end 负值代表 0
substr(start, length) start 开始获取长为 length 的字符串 允许 start 为负数

substr可能某些浏览器中无法使用 开发中推荐使用slice函数

在 JavaScript 字符串相关方法中,负数索引的含义如下:

  • slice(start, end) :允许负数索引。start为负数时,表示从字符串末尾往前数的位置,比如-1代表倒数第 1 个字符所在位置 ;end为负数时,也是从字符串末尾往前数,是截取结束位置(不包含)。例如"hello".slice(-3, -1)-3表示从后往前数第 3 个字符(即l),-1表示从后往前数第 1 个字符(即o),结果是"ll"
  • substr(start, length) :允许start为负数。此时start表示从字符串末尾往前数的位置 ,如"hello".substr(-3, 2)-3是从后往前数第 3 个字符(即l),然后取长度为 2 的子串,结果是"ll"
  • substring(start, end) :负数索引会被当作 0 处理。比如"hello".substring(-1, 3) ,等同于"hello".substring(0, 3) ,结果是"hel"

操作六:字符串占位

分为占前面的空位和占后面的空位 顾名思义

1
2
padStart(占用位数,用什么占位)
padEnd(占用位数,用什么占位)

这样就可以实现一些占位显示的问题 比如

1
2
var str = "4"
console.log(str.padStart(5,"0"))

其他操作

方法六:拼接字符串

contact支持链式操作 即一直.contact

1
2
str.concat(str2, 字符串或者一系列字符串)
示例:`console.log("hello".concat("World"))

方法七:删除首尾空格

1
2
str.trim()
示例:console.log(" coderwhy ".trim())

方法九:字符串分割

1
str.split(separator, limit)
  • separator:以什么字符串进行分割,也可以是一个正则表达式;
  • limit:限制返回片段的数量;
1
2
var message = "my name is coderwhy"
console.log(message.split(" ", 3)) // ['my', 'name', 'is']

更多的字符串的补充内容,可以查看 MDN 的文档:

数组

跟cpp相似 一种有序集合 但其中的内容的类型并不受到限制

数组是一种特殊的对象类型 也是一种用来保存多个数据的数据结构

属性

length属性 用来获取数组的长度

注意 length属性是可写的 即可以进行扩容或截取

1
2
3
4
5
6
//1.手动增加一个大于原length的数值 则会增加数组的长度
names.length = 10

//2.当length值属性小于原length的时候 会截取相应长度
names.length = 0;
//当length设置为0 的时候 就可以直接把数组清空

遍历

遍历支持三种遍历形式

for循环 for…in遍历 for … of遍历

注意forin遍历得到的是数组的索引 即

1
2
3
for(var index in names) {
console.log(index,names[index])
}

而forof遍历得到的是数组的元素 即

1
2
3
for(var item of names) {
console.log(item)
}

创建

使用**[ ]中括号**来创建一个数组

1
var shuzu  = [1,2,3,4,5,6,7,8,9]

使用 类Array 来创建一个数组

创建的数组是一个空数组 括号内可以放数组内元素

1
var shuzu1 = new Array(1,2,3,4,5,6,7,8,9)

注意 跟cpp里的vector很像的是 如果括号内是单个的数字的话 那么会创建一个对应长度的空数组(都是undefined)

1
var shuzu1 = new Array(5)

修改跟cpp一样直接找到索引然后改变即可

1
names[9] = "why"

新增

方法一:(少用)

直接使用原数组中没有的索引 可以加到数组的后面

1
names[10] = "why"

当然这样的操作有其不安全的地方 所以只是允许这种操作 很少使用

方法二:在数组的尾部添加元素

跟cpp数据结构很像 使用push方法 push 方法中可以增加多个元素

1
2
var names = ["John", "Jane", "Jim", "Joe"];
names.push("Jane","jily")
方法三:在数组的头部添加元素(慢)
1
names.unshift("why","cobe")

删除

方法一(少用)
1
delete names[1];
方法二:在数组的尾部取出一个元素
1
2
names.pop();
//注意一次只能删除一个元素
方法三:在数组的头部取出一个元素(慢)
1
2
names.shift();
//同样的 一次只能删除一个元素

arr.splice方法

可以在任何位置完成数组增删改的需求

arr.splice 的语法结构如下:

1
array.splice(start[, deleteCount[, item1[, item2[, ...]]]])
  • start位置开始,处理数组中的元素;
  • deleteCount: 要删除元素的个数,如果为 0 或者负数表示不删除;
  • item1, item2, ...: 在添加元素时,需要添加的元素;
1
2
3
4
5
6
// 1.删除一个元素
arr.splice(1, 1)
// 2.新增两个元素
arr.splice(1, 0, "abc", "cba")
// 3.替换两个元素
arr.splice(1, 2, "kobe", "curry")

注意:这个方法会修改原数组

arr.slice 方法

用于对数组进行截取(类似于字符串的 slice 方法)。
语法:arr.slice([begin[, end]])

  • 包含begin元素,但不包含end元素。
    示例:
1
console.log(arr.slice(2, 3))

arr.concat 方法

创建一个新数组,其中包含来自于其他数组和其他项的值。
语法:var new_array = old_array.concat(value1[, value2[, ...[, valueN]]])

1
2
var newArr = arr.concat(["abc", "cba"], "nba")
console.log(newArr)

对于连接字符串 除了用contact之外 还可以使用展开运算符

如果直接push的话 会push进去数组 那么新数组就会变成一个二维数组 需要传进去数组的元素的话就需要**…展开运算符**

(ES6新增)

1
2
3
4
var names = ["John", "Jane", "Jim", "Joe"];
var newnames = ["hh","jj"]
names.push(...newnames)
console.log(names)

arr.join 方法

将一个数组的所有元素连接成一个字符串并返回这个字符串。
语法:arr.join([separator])
示例:

1
console.log(arr.join("-"))

join里面的字符是这个字符串的分隔符

1
2
var names = ["John", "Jane", "Jim", "Joe"];
console.log(names.join("-"))

这样就会输出John-Jane-Jim-Joe

查找元素

index includes

数组的元素都为基本类型的时候 使用index和includes即可

数组的查找中 index和includes方法和字符串中的这两个方法是完全相同的

这里就省略不写了

find方法

当数组的元素为对象类型的时候使用 find方法

find函数是一种高阶函数 即其会接受一个匿名函数 其底层逻辑是浏览器内核会不断在这个数组中回调这个函数 直到遍历完整个数组 所以其实它很像是省略了手写的for循环 让浏览器自动循环遍历数组 并且每次遍历数组的时候都执行一次所写的函数

下面来看一个find函数的应用

1
2
3
4
5
6
7
8
9
10
11
12
13
var stu = [
{name:'张三',age:18}
,{name:'王五',age:19}
,{name:'李四',age:20}
,{name:'赵六',age:21}
]
stu.find(function(item){
console.log(item.name)
if(item.age==19) {
console.log("oh")
return true
}
})

这个时候会输出 张三 王五 oh

可以用手写的for循环来执行一下这个函数

1
2
3
4
5
6
7
for(var i = 0;i < stu.length;i++) {
console.log(stu[i].name)
if(stu[i].age === 19) {
console.log("oh")
break;
}
}

这下应该比较清楚了 反正find函数就是不断地在遍历过程中回调函数直到遍历结束

现在进一步介绍一下find方法

一般地 会在匿名函数中传入三个参数

1
stu.find(function(item,index,arr){})
  • item(必需 ):表示当前正在被处理的数组元素。在判断元素是否符合条件时,这是最核心的数据。
  • index(可选):代表当前正在被处理的元素在数组中的索引。当判断逻辑不仅依赖元素值,还和元素位置有关时会用到。
  • arr(可选):指调用find方法的数组本身。当回调函数内的逻辑需要基于整个数组来判断时会有用。比如判断当前元素是否是数组中最大的元素,就需要访问整个数组来比较。

并且这个方法默认返回的是查找到的元素本身

find方法的返回值

  • find方法返回的是第一个满足条件的元素(整个对象),而不是对象的某个属性。

    比如说

    1
    2
    3
    4
    var person = stu.find(function(item,index,arr){
    return item.age === 19
    })
    console.log(person)

这时候我们输出的person就会是

  1. Object

    1. age: 19
    2. name: “王五”

所以find方法是用来查找满足条件的元素本身的 对应的 我们也要查找元素索引的方法 即下面这个

findindex方法

findindex方法和find方法基本上一致 不同的地方就是它返回的是第一个满足条件的元素的索引

1
2
3
4
var personindex = stu.findIndex(function(item,index,arr){
return item.age === 19
})
console.log(personindex)

这时就会输出1 所以这个就不多说了

数组的排序

sort排序

跟cpp的sort排序还是比较像的 区别是它需要自己写匿名函数 这个匿名函数就有点类似于cpp里手写的那个cmp 用来自定义排序依据

默认的是

1
2
3
4
5
6
7
8
var nums = [9,3,8,7,6,4]
nums.sort(function(a,b){
return a - b //升序排序 谁小谁排在前面
})

nums.sort(function(a,b){
return b - a //降序排序 谁大谁排在前面
})

还可以使用

1
nums.reverse()

来倒序排序结果

Date类型

在 JavaScript 中我们使用 Date 来表示和处理时间。 所以下面的方法其实都是创建date类型的对象从而使用它里面的一些方法

创建

Date的构造函数有如下用法:

  • new Date();
  • new Date(value);
  • new Date(dateString);
  • new Date(year, monthIndex [, day [, hours [, minutes [, seconds [, milliseconds]]]]);

示例代码

1
2
3
4
5
6
7
8
9
10
11
// 创建Date对象
var date1 = new Date() // 当前时间 Fri May 13 2022 15:22:53 GMT+0800(伊尔库茨克标准时间)
var date2 = new Date(1000) // 传入的是毫秒数,表示从1970-01-01 00:00:00 UTC 经过的毫秒数
var date3 = new Date("2022-08-08") // 传入的是dateString,日期的字符串值
var date4 = new Date(2022, 08, 08, 08, 08, 08) // 传入具体的年月日时分秒毫秒
console.log(date4)

//传入一个Unix毫秒时间戳->它是一个整数值,表示1970 年 1 月 1 日 00:00:00 UTC以来的毫秒数
//可以通过时间戳来转化为当前的日期
var date = new Date(10000)
//如果不是毫秒的话记得换算

日期的表示方式

  • 日期的表示方式有两种:RFC 2822 标准 或者 ISO 8601 标准

  • 默认打印的时间格式:是 RFC 2822 标准的,例如new Date()输出Fri May 13 2022 17:14:52 GMT+0800

  • 转换为 ISO 8601 标准

    :可使用

    1
    new Date().toISOString()

    ,输出类似

    1
    '2022-05-13T09:15:51.507Z'

    • YYYY:年份,取值范围 0000 ~ 9999 。
    • MM:月份,取值范围 01 ~ 12 。
    • DD:日,取值范围 01 ~ 31 。
    • T:分隔日期和时间,无特殊含义,可省略 。
    • HH:小时,取值范围 00 ~ 24 。
    • mm:分钟,取值范围 00 ~ 59 。
    • ss:秒,取值范围 00 ~ 59 。
    • .sss:毫秒 。
    • Z:时区 。

获取信息的方式

  • 我们可以从 Date 对象中获取各种详细的信息:
    • getFullYear(): 获取年份(4 位数);
    • getMonth(): 获取月份,从 011
    • getDate(): 获取当月的具体日期,从 131 (方法名字有点迷);
    • getHours(): 获取小时;
    • getMinutes(): 获取分钟;
    • getSeconds(): 获取秒钟;
    • getMilliseconds(): 获取毫秒;
    • getDay():获取一天中的第几天 (注意这里

上面的每一种获取方法也有对应的设置方法 比如说 setMilliseconds()就能设置Date部分 并且 如果设置超过范围的数值 它会自动校准

Unix 时间戳

它是一个整数值,表示自 1970 年 1 月 1 日 00:00:00 UTC 以来的毫秒数。

在 JavaScript 中,获取这个时间戳的多种方法:

  • 方式一new Date().getTime()
  • 方式二new Date().valueOf()
  • 方式三+new Date()
  • 方式四Date.now()

获取到 Unix 时间戳之后,利用它来测试代码的性能示例:

1
2
3
4
5
6
var startTime = Date.now()
for (var i = 0; i < 10000; i++) {
console.log(i)
}
var endTime = Date.now()
console.log(endTime - startTime)

Date.parse 方法

  • Date.parse(str) 方法可以从一个字符串中读取日期,并且输出对应的 Unix 时间戳。

  • Date.parse(str)
    
    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
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60



    - 作用等同于 `new Date(dateString).getTime()` 操作;
    - 需要符合 RFC2822 或 ISO 8601 日期格式的字符串;
    - 比如`YYYY-MM-DDTHH:mm:ss.sssZ`;
    - 其他格式也许也支持,但结果不能保证一定正常;
    - 如果输入的格式不能被解析,那么会返回 NaN;



    ## JavaScript中的DOM操作

    DOM:文档对象模型(Document Object Model)

    - 简称 `DOM`,将页面所有的内容表示为可以修改的对象。html中的每一个元素都被抽象成了一个一个的对象

    即可以作为html,css和js之间的桥梁

    BOM:浏览器对象模型(Browser Object Model)

    - 简称 `BOM`,由浏览器提供的用于处理文档(document)之外的所有内容的其他对象;
    - 比如`navigator`、`location`、`history`等对象;



    ### DOM的继承关系图

    ![image-20250513171808350](C:/Users/Lenovo/Desktop/笔记/JavaScript.assets/image-20250513171808350.png)

    这里的继承跟之前java cpp里面学的继承是基本相同的 即 也会继承父级的特性 所以 需要通过这个继承关系 将一些特性联系起来



    **HTML节点和元素之间的区别**

    在 HTML 中,节点(Node)和元素(Element)是相关但有区别的概念:

    **定义与范畴**

    - 节点:
    - 节点是一个更为宽泛的概念,是构成网页 DOM(文档对象模型)树的基本单元。可以把整个 HTML 文档看作是一棵由节点组成的树,网页中的每一个部分,包括文档本身、HTML 标签、标签中的属性、标签内的文本内容、注释等,都可以被视为一个节点 。
    - 节点类型多样,常见类型有:
    - **文档节点**:代表整个 HTML 文档,是 DOM 树的根节点,在 JavaScript 中可通过`document`对象访问 。
    - **元素节点**:对应 HTML 中的标签,如`<div>`、`<p>`、`<a>`等。
    - **属性节点**:表示 HTML 元素的属性,例如`<a href="https://example.com">`中的`href`属性 。
    - **文本节点**:包含 HTML 元素内的文本内容,比如`<p>这是一段文本</p>`中的 “这是一段文本” 。
    - **注释节点**:HTML 中的注释部分,如`<!-- 这是一条注释 -->` 。
    - 元素:
    - 元素是节点的一种特定类型,专指 HTML 标签及其包含的内容(如果有的话) 。比如`<div class="container"><p>内容</p></div>`中,`<div>`标签及其中的`<p>`标签和文本等构成了一个元素节点。所有的 HTML 元素都是元素节点,但节点不一定是元素节点 ,如属性节点、文本节点就不属于元素范畴 。

    ### 关系与包含

    - 元素一定是节点,因为元素节点是节点类型中的一种。但节点不一定是元素 ,例如文本节点、属性节点等就不是元素。
    - 一个元素节点可以包含其他节点,比如子元素节点、文本节点等 。例如在`<div><p>段落文本</p></div>`中,`<div>`元素节点包含了`<p>`元素节点和 “段落文本” 这个文本节点 。

    ### document 对象

    - Document 节点表示的整个载入的网页,它的实例是全局的

    document
    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
    27
    28
    29
    30
    31
    32
    33

    对象:

    - 对 DOM 的所有操作都是从`document`对象开始的;
    - 它是 DOM 的入口点,可以从`document`开始去访问任何节点元素;

    - 对于最顶层的 html、head、body 元素,我们可以直接在document

    对象中获取到:

    - html 元素:`<html>` = `document.documentElement`
    - body 元素:`<body>` = `document.body`
    - head 元素:`<head>` = `document.head`
    - 文档声明:`<!DOCTYPE html>` = `document.doctype`

    但其实很多时候我们想要操作的不是这些很宽泛的元素 而是其中的某一个节点 所以还要使用的导航

    #### 节点之间的导航

    - 如果我们获取到一个节点(Node)后,可以根据这个节点去获取其他的节点,我们称之为节点之间的导航。

    - 节点之间存在如下的关系:

    - 父节点:`parentNode`

    - 前兄弟节点:`previousSibling`

    - 后兄弟节点:`nextSibling`

    - 子节点:`childNodes` 这个子节点可以作为一个数组使用索引 即

    ```javascript
    var h1Element = document.body.childNodes[0]
    - 第一个子节点:`firstChild` 注意对于节点来说 连换行符 空格这些也是算作一个节点的 所以说 有时候第一个子节点会是我们根本没有意识到的空格换行符等 特别要注意这个点 所以元素索引是更常用的 - 第二个子节点:`lastChild`

这些都见名知意 这里就不再写了

1
2
var bodyElement = document.body.firstChild
console.log(h1Element)

元素之间的导航

  • 如果我们获取到一个元素(Element)后,可以根据这个元素去获取其他的元素,我们称之为元素之间的导航。

  • 节点之间存在如下的关系:

    • 父元素:parentElement

    • 前兄弟元素:previousElementSibling

    • 后兄弟元素:nextElementSibling

    • 子元素:children 同样地 可以作为一个数组使用索引

      1
      var h1Element = document.body.children[0]
    • 第一个子元素:firstElementChild

    • 第二个子元素:lastElementChild

表格元素的导航

  • < table >元素支持以下这些属性:
    • table.rows<tr>元素的集合;
    • table.caption/tHead/tFoot — 引用元素<caption><thead><tfoot>
    • table.tBodies<tbody>元素的集合;
  • 元素提供了rows属性: - `tbody.rows` — 表格内部``元素的集合;
  • - `tr.cells` — 在给定``中的``和``单元格的集合; - `tr.sectionRowIndex` — 给定的``在封闭的`//`中的位置(索引); - `tr.rowIndex` — 在整个表格中``的编号(包括表格的所有行);

表单元素的导航

  • < form >元素可以直接通过document来获取:document.forms

    1
    var formEl = document.forms[0]
  • < form >元素中的内容可以通过elements来获取:form.elements

    1
    var elements = formEl.elements
  • 我们可以设置表单子元素的name来获取它们

方式一:直接通过表单实例访问

1
2
3
4
5
6
7
8
9
<form id="myForm">
<input name="email" type="email">
</form>

<script>
const form = document.getElementById('myForm');
const emailInput = form.elements.email; // 通过name获取元素
console.log(emailInput.value);
</script>

方式二:通过document.forms访问

1
2
3
4
5
6
7
8
<form name="userForm">
<input name="password" type="password">
</form>

<script>
const passwordInput = document.forms.userForm.elements.password;
console.log(passwordInput.value);
</script>

方式三:处理同名元素(如复选框、单选按钮)

当多个元素使用相同的name(如单选按钮组或复选框组),elements[name]会返回一个集合。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<form>
<input type="radio" name="gender" value="male">
<input type="radio" name="gender" value="female">
</form>

<script>
const form = document.forms[0];
const genderRadios = form.elements.gender; // 返回NodeList集合
for (const radio of genderRadios) {
if (radio.checked) {
console.log(radio.value); // 输出选中的值
}
}
</script>

获取元素的方法

当元素彼此靠近或者相邻时,DOM 导航属性(navigation property)非常有用。但在实际开发中,我们常希望能任意获取某一个元素。DOM 提供了以下获取元素的方法:

方法名 搜索方式 可以在元素上调用? 实时的?
querySelector CSS - selector -
querySelectorAll CSS - selector -
getElementById id - -
getElementsByName name -
getElementsByTagName tag or ‘*’
getElementsByClassName class

解释:

  • querySelector:使用 CSS 选择器来选取文档中与该选择器匹配的第一个元素,可在元素上调用。

  • querySelectorAll:使用 CSS 选择器来选取文档中与该选择器匹配的所有元素,可在元素上调用。 得到 可迭代对象 节点列表(不是数组)

    上面这两种最常用ovo

  • getElementById:通过元素的 id 属性值来获取对应的单个元素,不能在元素上调用。

  • getElementsByName:通过元素的 name 属性值来获取对应的元素集合,结果是实时的。

  • getElementsByTagName:通过元素的标签名来获取对应的元素集合,也可使用通配符*,可在元素上调用,结果是实时的。

  • getElementsByClassName:通过元素的类名来获取对应的元素集合,可在元素上调用,结果是实时的。

节点的属性

节点的类型 - nodeType

目前,我们已经可以获取到节点了,接下来我们来看一下节点中有哪些常见的属性。当然,不同的节点类型有可能有不同的属性,这里我们主要讨论节点共有的属性。

nodeType 属性

  • nodeType 属性提供了一种获取节点类型的方法。
  • 它有一个数值型值(numeric value)。

常见的节点类型

常量 描述
Node.ELEMENT_NODE 1 一个元素节点,例如 <p><div>
Node.TEXT_NODE 3 Element 或者 Attr 中实际的文字
Node.COMMENT_NODE 8 一个 Comment 节点。(注释)
Node.DOCUMENT_NODE 9 一个 Document 节点。
Node.DOCUMENT_TYPE_NODE 10 描述文档类型的 DocumentType 节点。例如 <!DOCTYPE html> 就是用于 HTML5 的。

其他类型可以查看 MDN 文档:https://developer.mozilla.org/zh-CN/docs/Web/API/Node/nodeType

节点的名称- nodeName、tagName

  • nodeName:用于获取 node 节点的名字。
  • tagName:用于获取元素的标签名词。
1
2
3
4
var textNode = document.body.firstChild;
var itemNode = document.body.childNodes[3];
console.log(textNode.nodeName);
console.log(itemNode.nodeName);

tagName 和 nodeName 的区别

  • 适用范围:
    • tagName属性仅适用于Element节点。
    • nodeName是为任意Node定义的:
      • 对于元素节点,它的意义与tagName相同,使用哪一个都可以。
      • 对于其他节点类型(如textcomment等),它是一个对应节点类型的字符串。

节点内容的获取

1
2
3
4
5
6
7
8
9
10
11
12
// 2.3. data(nodeValue)/innerHTML/textContent
// data针对非元素的节点获取数据
// innerHTML: 对应的html元素也会获取
// textContent: 只会获取文本内容
console.log(commentNode.data, textNode.data, divNode.data)
console.log(divNode.innerHTML) //会带着标签一起输出
console.log(divNode.textContent)

// 设置文本,作用是一样
// 设置文本中包含元素内容,那么innerHTML浏览器会解析,textContent会当成文本的一部分
divNode.innerHTML = "<h2>呵呵呵呵</h2>"
divNode.textContent = "<h2>嘿嘿嘿嘿</h2>"

元素的特性(attribute)

元素除了标签之外还存在很多属性 这些属性就称为元素的特性attribute

attribute分为两类 一类是标准的attribute即html中自带的像id class 这些属性

一类是自定义的attribute即自己定义的一些属性

常见操作

以下这些操作对于所有的attribute都适用

attribute 的操作

对于所有的 attribute 访问都支持如下的方法:

  • elem.hasAttribute(name):检查特性是否存在。
  • elem.getAttribute(name):获取这个特性值。
  • elem.setAttribute(name, value):设置这个特性值。
  • elem.removeAttribute(name):移除这个特性。
  • attributes:attr 对象的集合,具有 name、value 属性。
1
2
3
4
5
6
7
for (var attr of boxEl.attributes) {
console.log(attr.name, attr.value)
}
console.log(boxEl.hasAttribute("age"))
console.log(boxEl.getAttribute("name"))
boxEl.setAttribute("name", "kobe")
boxEl.removeAttribute("abc")

attribute 具备以下特征

  • 它们的名字是大小写不敏感的(id 与 ID 相同)。
  • 它们的值总是字符串类型的。

对象的特性(property)

property指的是对象的属性 比如说

1
2
3
4
5
var obj = {
name: 'zhangsan',
age: 18,
sex: 'man'
}

此时这里的name age sex都属于对象的属性property 那么同样是属性 attribute和property之间的关系是什么呢

我们知道 当我们用DOM去获取HTML中的某个元素的时候 这个元素会被转换成对象类型让我们来进行操作

那么 对于标准attribute 都会在对象中生成 对应的property 故而我们可以直接像调用对象属性一样 用 点. 操作符来调用这些attribute的值甚至做一些改变

1
2
3
4
5
6
7
8
<body>
<div id ="ID" title = "标题" class = "box">i am the box</div>
<script>
var boxEl = document.querySelector('.box')
boxEl.id = 'newID'
console.log(boxEl.id)
</script>
</body>

从这里可以看到 我们不仅能用这种方式进行调用 还可以通过这种方法进行修改

并且 property具有的优势是 它返回的值并不像attribute那样都是字符串 而是根据属性而返回 比如说checked就会返回布尔值 便于进行一些后续的判断操作

所以

除非特殊情况 大多数情况下 设置 获取attribute都更推荐使用property方式 因为它在默认情况下都是有类型的

JavaScript动态修改样式

元素的 className 和 classList

  • className:元素的 class attribute,对应的 property 并非叫 class,而是 className。

    • 原因:JavaScript 早期不允许使用 class 这种关键字作为对象的属性,所以 DOM 规范使用了 className;虽现在已无此限制,但不推荐使用 class,仍沿用 className。

    • 赋值效果:对 className 进行赋值,会替换整个类中的字符串 替换原来的class。

      即它可以在某些需求后 直接改变原元素的class属性 从而达到状态的切换 一般需要我们先用css写一个点击状态下的样式

    • 示例代码

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      <!DOCTYPE html>
      <html lang="en">
      <head>
      <meta charset="UTF-8">
      <meta name="viewport" content="width=device-width, initial-scale=1.0">
      <title>Document</title>
      <style>
      .active{
      color: red;
      background-color: aqua;
      font-size: 20px;
      }
      </style>
      </head>
      <body>
      <div class = "box">我是盒子</div>
      <script>
      var boxEl = document.querySelector('.box')
      boxEl.onclick = function(){
      boxEl.className = "active"
      }
      </script>
      </body>
      </html>

      这样就能实现一个点击状态后的切换

    • 注意使用classname的弊端是 会直接替换掉原来的class属性 不能够进行点击前后属性的切换 所以有下面这个方法

  • classList:如果需要添加或者移除单个的 class,可使用 classList 属性。elem.classList 是一个特殊对象,有以下方法:

    • **elem.classList.add(class)**:添加一个类。
    • **elem.classList.remove(class)**:移除类。
    • **elem.classList.toggle(class)**:如果类不存在就添加类,存在就移除它。
    • **elem.classList.contains(class)**:检查给定类,返回 true/false。

classList是一个可迭代对象 可以用for of 遍历所有class

如果只改变单个的css样式的话 就可以用property中的style属性 来调整

1
2
3
4
5
6
7
8
9
boxEl.style.backgroundColor = red
//注意这里的细节 当样式单词是多个单词的时候 这里使用小驼峰命名法

//如果将一个属性的值设置为空的字符串的话 那么使用默认值 即浏览器的默认属性 或者是一开始设定的初始化属性
boxEl.style.display = ""
boxEl.style.fontSize = ""

//用style设置多个样式
boxEl.style.cssTexst = 'color:red;background-color:aqua;font-size:20px'

style的获取

在前端开发中,操作元素样式时,会遇到读取样式的需求。

常规读取方式的局限

  • 对于内联样式,可以通过 element.style.属性名 的方式读取。例如有一个 <div id="box" style="width: 100px;"></div> ,在 JavaScript 中可以通过 document.getElementById('box').style.width 读取其宽度。
  • 但对于写在 <style> 标签内或者外部 CSS 文件中的样式,无法通过上述 element.style.属性名 的方式读取。

getComputedStyle 函数

getComputedStyle 是一个全局函数,用于获取元素的计算样式。计算样式是指浏览器经过样式层叠等规则计算后,元素最终应用的样式。

  • 语法getComputedStyle(element, [pseudoElt]) ,其中 element 是要获取样式的 DOM 元素;pseudoElt 可选,用于指定伪元素(如 :before:after ),一般传 null
  • 示例代码
1
2
3
4
5
6
7
8
// 假设HTML中有一个 <div id="boxE1"></div>
const boxE1 = document.getElementById('boxE1');
// 读取宽度
console.log(getComputedStyle(boxE1).width);
// 读取高度
console.log(getComputedStyle(boxE1).height);
// 读取背景颜色
console.log(getComputedStyle(boxE1).backgroundColor);

getComputedStyle 会返回一个包含元素所有计算后样式属性的对象,通过该对象可以方便地获取元素实际应用的样式,解决了无法直接读取非内联样式的问题 。

dataset的使用

对于一些自定义的属性的话 我们一般需要写成这样的格式

1
data-age = "18"  data-height = "1.88"

对于这些属性 都会被放进property中的dataset属性中 这样就可以直接通过dataset来获取自定义的属性

1
2
3
4
<div class = "box" data-age = "18">我是盒子</div>
<script>
console.log(boxEl.dataset.age)
</script>

插入元素

一般是指我们通过js向html中插入一些元素

最原始的 在没有DOM 的时候 我们可以使用

1
document.write

的方法来写入一个元素 但缺点是很难完成元素的拼接操作或者写入一些复杂的内容 所以 当我们需要 插入元素 的时候 一般的想法是

先创建一个元素 然后插入元素到DOM的某一个位置

创建元素

真实创建一个DOM对象

document.createElement(tag)

即括号内放的是想要创建的元素的标签 我们可以通过元素的操作来给这个元素添加属性

1
2
3
4
var h2El = document.createElement('h2')
h2El.innerHTML = '我是h2'
h2El.className = "title"
h2El.classList.add('active')

这样就比较完整地创建了一个真实DOM对象即元素

插入元素

插入元素的方式如下:

其中 node指的是插入操作针对的那个对象节点 注意是节点 也就是说适用性是很高的 当然为了确保准确性 一般我们获取这个node的时候可以单独给其加一个class属性

并且插入的内容可以是单个内容 也可以是多个内容

  • node.append(...nodes or strings) —— 在 node 末尾插入节点或字符串。
  • node.prepend(...nodes or strings) —— 在 node 开头插入节点或字符串。
  • node.before(...nodes or strings) —— 在 node 前面插入节点或字符串。
  • node.after(...nodes or strings) —— 在 node 后面插入节点或字符串。
  • node.replaceWith(...nodes or strings) —— 将 node 替换为给定的节点或字符串

移除,克隆元素

移除元素

移除元素调用元素对象本身的remove方法即可

1
2
3
4
5
6
7
<div class = "box" data-age = "18">我是盒子</div>
<script>
var boxEl = document.querySelector('.box');
boxEl.onclick = function(){
boxEl.remove()
}
</script>

克隆元素

克隆元素调用元素对象的 cloneNode方法 此方法有如下细节

  • 可以传入一个布尔类型的值 来决定是否是深度克隆

  • 深度克隆会克隆对应元素的子元素 如果不是深度克隆的话则不会

  • cloneNode方法克隆的是节点 所以是包含注释文本等内容的

  • 克隆对象的操作等等是跟普通元素对象一样的

  • 如果要显示的话 克隆后记得插入

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    <div class = "box" data-age = "18">
    我是盒子
    <h1>我是第一个</h1>
    <h2>我是第二个</h2>
    </div>
    <script>
    var boxEl = document.querySelector('.box');
    boxEl.onclick = function(){
    var newbox = boxEl.cloneNode(true) // 创建出的是一个新对象
    newbox.childNodes[1].classList.add('active') //深度克隆的话 这个新对象也一样可以对其子代进行操作
    boxEl.after(newbox) // 在原对象之后插入新对象 注意克隆后要记得插入
    }
    </script>

元素的大小、滚动

  • clientWidth:contentWidth + padding(不包含滚动条)
  • clientHeight:contentHeight + padding
  • clientTop:border - top 的宽度
  • clientLeft:border - left 的宽度
  • offsetWidth:元素完整的宽度
  • offsetHeight:元素完整的高度
  • offsetLeft:距离父元素的 x
  • offsetHeight:距离父元素的 y (这里你图片中 offsetHeight 对距离父元素 y 的表述可能有误,正常表述距离父元素 y 的应该是 offsetTop
  • scrollHeight:整个可滚动的区域高度
  • scrollTop:滚动部分的高度

window 的大小、滚动

window 的 width 和 height

  • innerWidthinnerHeight:获取 window 窗口的宽度和高度(包含滚动条)
  • outerWidthouterHeight:获取 window 窗口的整个宽度和高度(包括调试工具、工具栏)
  • documentElement.clientHeightdocumentElement.clientWidth:获取 html 的宽度和高度(不包含滚动条)

window 的滚动位置

  • scrollX:X 轴滚动的位置(别名 pageXOffset)
  • scrollY:Y 轴滚动的位置(别名 pageYOffset)

对应的滚动方法

  • 方法 scrollBy(x,y):将页面滚动至相对于当前位置的 (x, y) 位置;
  • 方法 scrollTo(pageX,pageY):将页面滚动至绝对坐标。

事件

事件流- 事件冒泡和事件捕获

默认情况下事件是从最里层的子代元素依次传递到最外层的父代元素 这个顺序称之为 事件冒泡

还有另一种监听事件流的方式是从外层到内层 这种顺序称之为 事件捕获

事件对象

比如鼠标点击滑动等操作就称之为事件 当一个事件发生的时候 就会有很多跟这个事件有关的信息 比如事件的类型 点击元素 位置等 这些信息回被封装到一个 Event对象中 这个对象由浏览器建立

常见属性
  • type:事件的类型;
  • target:当前事件发生的元素;- > 原本监听的事件 即事件发生的对象
  • currentTarget:当前处理事件的元素; - > 当前处理的对象 即捕获或冒泡的最终结果
  • eventPhase:事件所处的阶段;
  • offsetX、offsetY:事件发生在元素内的位置;
  • clientX、clientY:事件发生在客户端内的位置;
  • pageX、pageY:事件发生在客户端相对于 document 的位置;
  • screenX、screenY:事件发生相对于屏幕的位置;
常见的方法:
  • preventDefault:取消事件的默认行为;
  • stopPropagation:阻止事件的进一步传递(冒泡或者捕获都可以阻止);

事件处理中的this

同样 在事件处理中的函数也可以调用this来获取当前的发生元素 即 监听的是哪个事件 最终this指向的就是哪个事件

EventTarget 类

  • 我们会发现,所有的节点、元素都继承自 EventTarget
    • 事实上,Window也继承自EventTarget;[EventTarget -> Window]
  • 那么这个 EventTarget 是什么呢?
    • EventTarget 是一个 DOM 接口,主要用于添加、删除、派发 Event 事件;
  • EventTarget 常见的方法:
    • addEventListener:注册某个事件类型以及事件处理函数; 这个方法可以用于给一个事件添加多种事件处理函数
    • removeEventListener:移除某个事件类型以及事件处理函数;
    • dispatchEvent:派发某个事件类型到 EventTarget 上;
1
2
3
4
var boxEl = document.querySelector(".box")
boxEl.addEventListener("click", function() {
console.log("点击了box")
})
1
2
3
4
5
6
boxEl.addEventListener("click", function() {
window.dispatchEvent(new Event("coderwhy"))
})
window.addEventListener("coderwhy", function(event) {
console.log("监听到coderwhy事件:", event)
})

事件委托

事件委托的基本原理是事件冒泡

在事件冒泡中可知 子代的事件会逐渐传递到父代中去 故而 我们可以 在父代上绑定一个监听器 这样其任何一个子代发生事件的时候都会最终传递到这个父代上 从而进行我们想进行的操作 这样就可以极大程度上减少代码量 避免对每一个子代都单独进行监听

那么传递到父代后又是怎么准确定位到是哪一个子代发生的事件呢

可以使用 target属性 准确定位到是哪一个事件发生了监听

现在就事件委托写一段小代码

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
27
28
29
30
31
32
33
34
35
36
37
38
39
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>

<style>
.active{
background: red;
}
</style>
</head>
<body>
<ul>
<li>1</li>
<li>2</li>
<li>3</li>
<li>4</li>
<li>5</li>
</ul>

<script>
var ul = document.querySelector('ul');
var findpush = null;
ul.onclick = function(){ //事件委托 给父代添加监听
if(findpush) {
findpush.classList.remove('active');
} //把之前的高亮部分去除

if(event.target.tagName === 'LI'){ //通过event定位
event.target.classList.add('active');
}

findpush = event.target;
}
</script>
</body>
</html>

常见的鼠标事件

属性 描述
click 当用户点击某个对象时调用的事件句柄。
contextmenu 在用户点击鼠标右键打开上下文菜单时触发
dblclick 当用户双击某个对象时调用的事件句柄。
mousedown 鼠标按钮被按下。
mouseup 鼠标按键被松开。
mouseover 鼠标移到某元素之上。(支持冒泡)
mouseout 鼠标从某元素移开。(支持冒泡)
mouseenter 当鼠标指针移动到元素上时触发。(不支持冒泡)
mouseleave 当鼠标指针移出元素时触发。(不支持冒泡)
mousemove 鼠标被移动。
区别

mouseenter 和 mouseleave

  • 不支持冒泡 - > mouseenter不能用来进行事件委托
  • 进入子元素依然属于在该元素内,没有任何反应 即不会对嵌套的其他元素产生效果

mouseover 和 mouseout

  • 支持冒泡
  • 进入元素的子元素时
    • 先调用父元素的 mouseout
    • 再调用子元素的 mouseover
    • 因为支持冒泡,所以会将 mouseover 传递到父元素中

所以它们的区别很大程度上要聚焦到能否进行事件委托上

常见的键盘事件

属性 描述
onkeydown 某个键盘按键被按下。
onkeypress 某个键盘按键被按下。
onkeyup 某个键盘按键被松开。
事件执行顺序

事件的执行顺序是 onkeydown、onkeypress、onkeyup:

  • down 事件先发生;
  • press 发生在文本被输入;
  • up 发生在文本输入完成(抬起 松开) 。
区分按键的方式

我们可以通过 key 和 code 来区分按下的键:

  • code:“按键代码”(”KeyA”, “ArrowLeft” 等),特定于键盘上按键的物理位置。
  • key:字符(”A”, “a” 等),对于非字符(non - character)的按键,通常具有与 code 相同的值。

常见的表单事件

针对表单也有常见的事件:

属性 描述
onchange 该事件在表单元素的内容改变时触发(<input>, <keygen>, <select>, 和 <textarea>) 元素失去焦点的时候才会触发哦
oninput 元素获取用户输入时触发
onfocus 元素获取焦点时触发
onblur 元素失去焦点时触发
onreset 表单重置时触发
onsubmit 表单提交时触发

文档加载事件

  • DOMContentLoaded:浏览器已完全加载 HTML,并构建了 DOM 树,但像 <img> 和样式表之类的外部资源可能尚未加载完成。
  • load:浏览器不仅加载完成了 HTML,还加载完成了所有外部资源:图片,样式等。
1
2
3
4
5
6
7
8
9
10
11
12
13
<div>哈哈哈</div>
<img src="https://ossweb-img.qq.com/upload/webplat/info/yxzj/20220510/28345686899485.jpg" alt="">

<script>
window.addEventListener("DOMContentLoaded", function() {
var imgEl = document.querySelector("img")
console.log("页面内容加载完毕", imgEl.offsetWidth, imgEl.offsetHeight)
})
window.addEventListener("load", function() {
var imgEl = document.querySelector("img")
console.log("页面所有内容加载完毕", imgEl.offsetWidth, imgEl.offsetHeight)
})
</script>

window 定时器方法

有时我们并不想立即执行一个函数,而是等待特定一段时间之后再执行,我们称之为 “计划调用(scheduling a call)” 。

实现方法

目前有两种方式可以实现:

  • setTimeout :允许我们将函数推迟到一段时间间隔之后再执行。

    1
    2
    3
    4
    5
    function foo(...message) {
    console.log(message)
    }
    setTimeout(foo, 1000, 'hello world','hahahah')
    //setTimeout(想要执行的函数或代码字符串, delay即延迟时间单位为毫秒, 要传入函数的参数列表 如果要传入多个的话记得写...展开符)
  • setInterval :允许我们重复运行一个函数,从一段时间间隔之后开始运行,之后以该时间间隔连续重复运行该函数。

    用法和上面的完全相同 不同点只是中间的数字代表的是每次执行的时间间隔

取消方法

并且通常情况下有提供对应的取消方法:

  • clearTimeout :取消 setTimeout 的定时器;

    setTimeout在调用的时候会返回一个定时器标识符 可以用它来取消执行

    1
    2
    3
    4
    var timerId = setInterval(function() {
    console.log('hello world')
    }, 1000)
    clearTimeout(timerId)
  • clearInterval :取消 setInterval 的定时器;

    用法和操作方法跟上面的完全相同 这里就略去了

大多数运行环境都有内置的调度程序,并且提供了这些方法:

  • 目前来讲,所有浏览器以及 Node.js 都支持这两个方法;
  • 所以我们后续学习 Node 的时候,也可以在 Node 中使用它们;

JavaScript中的BOM操作

BOM:浏览器对象模型(Browser Object Model)

  • 简称 BOM,由浏览器提供的用于处理文档(document)之外的所有内容的其他对象;

  • 比如 navigatorlocationhistory 等对象;

    JavaScript 运行环境与浏览器

  • JavaScript 有一个非常重要的运行环境就是浏览器

  • 而且浏览器本身又作为一个应用程序需要对其本身进行操作;

  • 所以通常浏览器会有对应的对象模型(BOM,Browser Object Model);

  • 我们可以将 BOM 看成是连接 JavaScript 脚本与浏览器窗口的桥梁;

BOM 主要包括的对象模型

  • window:包括全局属性、方法,控制浏览器窗口相关的属性、方法;
  • location:浏览器连接到的对象的位置(URL);
  • history:操作浏览器的历史;
  • navigator:用户代理(浏览器)的状态和标识(很少用到);
  • screen:屏幕窗口信息(很少用到);

window 对象

window 对象的两个视角

在浏览器环境里,window对象可从以下两个视角理解:

  • 视角一:全局对象
    ECMAScript 有全局对象,在 Node 环境中是global ;而在浏览器里,这个全局对象就是window ,它承载着全局作用域相关内容,让全局定义的变量、函数等能依托它存在。
  • 视角二:浏览器窗口对象
    作为浏览器窗口的代表,window提供了操作浏览器窗口的 API ,像控制窗口大小、获取窗口位置、操作浏览器历史记录等功能,都可通过它实现。

无需刻意区分的原因

这两个视角存在大量重叠,实际使用无需严格区分,还有这些特点:

  • 统一标准 globalThis:为解决浏览器(window )和 Node(global )全局对象名称差异,推出globalThis ,现代浏览器多已支持,方便跨环境统一访问全局对象。
  • 属性访问便捷window上的属性可直接访问,不用显式写window. 前缀 ;用var定义的变量,会自动添加到window对象中,成为其属性。
  • 内置全局内容window默认提供诸多全局函数和类,例如setTimeout(定时任务)、Math(数学运算)、Date(日期处理)、Object(对象基础操作)等,助力日常开发。

location 对象

location 对象用于表示 window 上当前链接到的 URL 信息(可以简单理解为完整的链接),其常见属性如下:

  • href:当前 window 对应的超链接 URL,涵盖完整 URL 内容。
  • protocol:当前 URL 使用的协议,如 httphttps 等。
  • host:主机地址,包含域名(或 IP)与端口 。
  • hostname:主机地址(不带端口),仅域名或 IP 部分。
  • port:URL 对应的端口号 。
  • pathname:URL 中的路径部分,指定资源在服务器的位置。
  • search:查询字符串,即 URL 中 ? 后的参数部分 。
  • hash:哈希值,是 URL 中 # 后的片段,常用于前端路由等场景 。
  • username:URL 中的用户名(多数浏览器已禁用,因安全风险)。
  • password:URL 中的密码(多数浏览器已禁用,因安全风险)

我们会发现 location其实是URL的一个抽象实现

image-20250604180658125

常用方法

  • assign:为当前窗口赋值新 URL 并跳转,会在浏览历史中留下记录,支持 “后退” 返回原页面 。
    示例:location.assign('https://example.com')
  • replace:打开新 URL 并跳转,但不会在浏览历史留存当前页面记录,无法通过 “后退” 回到跳转前页面,适合无需回退的场景(如登录后跳转)。
    示例:location.replace('https://example.com/login')
  • reload:重新加载当前页面,可传布尔值参数:
    • reload()reload(false):默认用缓存刷新(快速,可能不重新请求资源)。
    • reload(true):强制从服务器重新加载(忽略缓存,资源全更新)。
      示例:location.reload(true)(强制刷新)

URLSearchParams

URLSearchParams 定义了一些实用的方法来处理 URL 的查询字符串。

这里重新说一下什么是查询字符串

查询字符串(Query String )是 URL(统一资源定位符 )中 ? 后面的部分,用于向服务器传递额外的参数信息 ,常用来做数据传递、筛选等。比如在 https://example.com/search?keyword=js&page=1 里,keyword=js&page=1 就是查询字符串,keywordpage 是参数名,js1 是对应参数值,多组参数用 & 分隔 ,方便服务器根据这些参数返回不同内容,像搜索结果、分页数据等,前端也能用 URLSearchParams 这类工具便捷操作它。

核心能力

  • 可将字符串转化为 URLSearchParams 类型;
  • 也能把 URLSearchParams 类型转回字符串。
1
2
3
var urlsearch = new URLSearchParams("name=why&age=18&height=1.88")  
console.log(urlsearch.get("name")) // why
console.log(urlsearch.toString()) // name=why&age=18&height=1.88

常见方法

  • get:获取搜索参数的值;
  • set:设置一个搜索参数和值;
  • append:追加一个搜索参数和值;
  • has:判断是否有某个搜索参数;

MDN 文档

编码解码

处理中文时,常用 encodeURIComponent 编码、decodeURIComponent 解码。

history对象

history 对象用于访问浏览器会话的历史记录,以下是核心内容:

1. 功能定位

history 对象允许访问浏览器曾经的会话历史记录,可控制页面跳转、管理历史状态。

2. 属性(2 个)

  • length:会话中的历史记录条数(比如打开过多少个页面,会统计在内 )。
  • state:当前保留的状态值(配合 pushState/replaceState 存储的自定义数据 )。

3. 方法(5 个)

  • **back()**:返回上一页,等价于 history.go(-1)(类似浏览器 “后退” 按钮 )。
  • **forward()**:前进下一页,等价于 history.go(1)(类似浏览器 “前进” 按钮 )。
  • **go()**:加载历史中的某一页,参数为数字(如 go(2) 前进 2 页,go(-2) 后退 2 页 )。
  • pushState():新增历史记录并跳转,不会真的刷新页面,可传状态数据(常用于前端路由 )。
  • **replaceState()**:用新记录替换当前历史记录,同样不刷新页面,适合修改当前状态场景。

4. 底层关联

history(结合 hash )是 Vue、React 等框架实现前端路由(简单来说就是修改URL 但是页面不刷新 也就是所谓的前端渲染)的底层原理,后续会展开讲具体实现 。

简单说,history 让前端能灵活控制页面 “前进后退”、管理路由状态,是单页应用(SPA)的基础工具之一~

navigator对象表示用户代理的状态和标识等信息

screen对象(很少用)

主要是记录浏览器窗口外面的客户端显示器的信息 比如说屏幕的逻辑像素

JSON

注意JSON不是一种变成语言

JSON(JavaScript Object Notation)即 JavaScript 对象表示法,是一种轻量级的数据交换格式,以易于人阅读和编写的文本格式来存储和表示数据,在前后端数据交互等场景广泛应用

JSON 的方法可以帮我实现对象的深拷贝

JSON 顶层

JSON 作为数据交换格式,顶层支持以下三类值:

  1. 简单值

包含基础数据类型,规则如下:

  • 类型:数字(Number)、字符串(String,仅双引号,不支持单引号 )、布尔类型(Boolean)、null 类型。
  • 示例:"name": "why", "age": 18, "isStudent": true, "desc": null
  1. 对象值

以键值对(key-value)结构组织,规则:

  • key 必须是带双引号的字符串

  • value 可以是简单值、对象值(嵌套对象 )、数组值(嵌套数组 )。

  • 示例:

    1
    2
    3
    4
    5
    6
    7
    {  
    "user": {
    "name": "why",
    "age": 18,
    "hobbies": ["coding", "reading"]
    }
    }
  1. 数组值

以数组形式组织数据,规则:

  • value 可以是简单值、对象值(数组元素为对象 )、数组值(嵌套数组 )。

  • 示例:

    1
    2
    3
    4
    5
    6
    7
    {  
    "scores": [
    95,
    {"subject": "math", "score": 98},
    [80, 85]
    ]
    }

这些类型覆盖了 JSON 最常用的结构,写接口数据、配置文件时经常用到~

JSON序列化

在某些情况下 我们希望把js中的一些复杂元素给转换为JSON类型的字符串 这样方便后续对其进行处理

在 JavaScript 中,JSON 序列化 / 反序列化主要通过 JSON 全局对象 实现,核心是 stringifyparse 两个方法,以下是关键说明:

  1. 核心方法(ES5 引入)

JSON 全局对象提供两个基础方法,用于 JavaScript 类型与 JSON 字符串的转换:

  • **JSON.stringify**:

    • 作用:把 JavaScript 数据类型(对象、数组等)转成 JSON 格式的字符串,方便存储 / 传输。

    • 示例:

      1
      2
      3
      const obj = { name: "why", age: 18 };  
      const jsonStr = JSON.stringify(obj);
      // 结果:'{"name":"why","age":18}'(字符串)
  • **JSON.parse**:

    • 作用:把 JSON 格式的字符串 解析回 JavaScript 类型(对象、数组等 ),用于还原数据。

    • 示例:

      1
      2
      3
      const jsonStr = '{"name":"why","age":18}';  
      const obj = JSON.parse(jsonStr);
      // 结果:{ name: "why", age: 18 }(对象)
  1. 典型应用场景:本地存储(LocalStorage)

因 LocalStorage 只能存字符串,结合 JSON 方法可实现对象 / 数组的存储与还原:

1
2
3
4
5
6
7
8
9
// 1. 存数据:对象 → JSON 字符串 → 存入 LocalStorage  
const obj = { name: "why", age: 18 };
const objString = JSON.stringify(obj);
localStorage.setItem("info", objString);

// 2. 取数据:从 LocalStorage 取字符串 → JSON.parse → 还原对象
const itemString = localStorage.getItem("info");
const info = JSON.parse(itemString);
console.log(info); // { name: "why", age: 18 }

简单说,stringify 负责 “转成字符串”,parse 负责 “解析回原类型”,二者配合就能轻松处理 JavaScript 数据与 JSON 字符串的转换,是前后端交互、本地存储的基础工具~

stringfy的补充

JSON.stringify() 不仅能简单转换对象为 JSON 字符串,还可通过 replacer 参数 实现筛选属性自定义替换逻辑

  1. replacer 参数:数组 → 筛选属性

replacer数组,则仅保留数组中指定的属性,其他属性会被过滤:

1
2
3
4
5
6
7
8
9
10
11
const obj = {  
name: "why",
age: 18,
friend: { name: "kobe" },
hobbies: ["篮球", "足球", "乒乓球"]
};

// replacer 为数组 → 仅保留 name、age
const objString2 = JSON.stringify(obj, ["name", "age"]);
console.log(objString2);
// 结果:'{"name":"why","age":18}'

​ 2.replacer 参数:函数 → 自定义替换

replacer函数,可对每个键值对自定义处理逻辑(返回值作为最终值 ):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const objString3 = JSON.stringify(obj, (key, value) => {  
// 调试:查看每个 key-value(空 key 对应整个对象)
console.log(key, value);

// 自定义替换:name 字段替换为 "coderwhy"
if (key === "name") {
return "coderwhy";
}

// 其他字段保留原值
return value;
});

console.log(objString3);
// 结果:'{"name":"coderwhy","age":18,"friend":{"name":"coderwhy"},"hobbies":["篮球","足球","乒乓球"]}'

但如果对象本身包含toJSON方法的话 那么会直接调用toJSON

1
2
3
4
5
6
7
8
9
var obj = {
name: 'hello world',
age: 18,
toJSON: function() {
return "123"
}
}
var objtojsonstring = JSON.stringify(obj)
console.log(objtojsonstring)

比如像现在这样的话无论对象是什么都只会返回123

parse方法的补充

跟stringfy类似的 也可以通过可选的reviver函数用于自定义转换

reviver 函数:自定义转换

reviver 是可选参数(函数),会在解析过程中对每个键值对做处理,返回值作为最终结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
const objString = '{"name":"why","time":"2025-06-04"}';  

// 解析时转换 time 字段为 Date 对象
const info2 = JSON.parse(objString, (key, value) => {
if (key === "time") {
// 将字符串转成 Date 类型
return new Date(value);
}
// 其他字段保持原值
return value;
});

console.log(info2.time); // Date 对象:Tue Jun 04 2025 ...