说说js的数据类型

js的数据类型可以分为两类,基本数据类型引用数据类型

基本数据类型

基本数据类型主要有6种:Number,String,Boolean,Symbol,Null,Undefined,后来又添加了一种叫做BigInt所以说基本数据类型就有7种。

Number

最常见的整数类型格式则为十进制,还可以设置八进制(零开头)、十六进制(0x开头)

1
2
3
4
5
6
let intNum = 55 // 10进制的55
let num1 = 070 // 8进制的56
let hexNum1 = 0xA //16进制的10
// 十六进制的 1A 转换为十进制是26
// A代表10,再乘以权重16^0,等于10,1乘以权重16^1等于16,相加得到26
let hexNumber = 0x1A;

浮点类型则在数值中必须包含小数点,还可通过科学计数法表示。

1
2
3
4
let floatNum1 = 1.1;
let floatNum2 = 0.1;
let floatNum3 = .1; // 有效,但不推荐
let floatNum = 3.125e7; // 等于 31250000

格式化

关于浮点数有一个重要的知识点就是格式化

  • 使用 toFixed() 方法

    表示保留几位小数,要注意的是不是format方法,在js中不是使用这种方法格式化浮点数

    1
    2
    let num = 123.456;
    console.log(num.toFixed(2)); // 输出 "123.46" - 四舍五入到两位小数

    toFixed() 返回的是一个字符串,而不是数字。如果需要进行进一步的数值计算,你可能需要将其转换回数字类型

  • 使用 toPrecision() 方法

    这个方法表示保留几位有效数字,并且会根据需要,自动调整数字的表示形式(科学记数法或固定点表示法)

    1
    2
    3
    let num = 123.456;
    console.log(num.toPrecision(3)); // 输出 "123"
    console.log(num.toPrecision(5)); // 输出 "123.46"
  • Number.prototype.toLocaleString()

    toLocaleString() 可以用于获取特定地区的数字格式,包括货币、百分比和日期格式等。对于浮点数格式化,它可以用来设置小数位数和使用逗号作为千位分隔符等。

    1
    2
    let num = 1123.456;
    console.log(num.toLocaleString())//输出1,123.456

NaN

在数值类型中,存在一个特殊数值NaN,意为“不是数值”,用于表示数值运算操作失败了,而不是抛出错误

1
2
console.log(0/0); // NaN
console.log(-0/+0); // NaN

存储空间

在 JavaScript 中,变量的声明方式(varletconst不会影响其占用的内存大小。内存占用主要取决于变量存储的数据类型,而不是声明关键字本身。

  • Number

    8 字节(64 位),因为所有数字都以双精度浮点数形式存储。

    1
    2
    let num = 123; // 占用 8 字节
    const pi = 3.14; // 占用 8 字节
  • BigInt:内存占用随整数大小动态变化。

    1
    const bigIntValue = 1234567890123456789012345678901234567890n; // 内存占用随值增大而增加
  • String:内存占用与字符串长度成正比,每个字符通常占用2 字节

    1
    const str = "Hello"; // 长度为 5 的字符串,占用约 10 字节
  • Boolean通常占用4 字节或更少(具体实现因引擎而异)。

    1
    const flag = true; // 占用少量固定内存
  • undefinednull:通常占用4 字节或更少。

    1
    2
    let x; // undefined,占用少量固定内存
    const y = null; // 占用少量固定内存

存储一个ip地址,如何实现存储空间最小?在c语言中,一个字符char,占用一个字节,用字符串存储一个ip地址,最多占用3*4+3(3个分隔符)=15个字节,但是因为ip地址每位的范围是0-255,用一个字节就能存储,所以用一个数组存储ip数组的各个部分,最多大概只需要占用4字节。

String

字符串使用双引号(”)、单引号(’)或反引号(`)表示都可以,反引号表示的是模板字符串,模板字符串和普通字符串有什么区别呢?

在模板字符串中可以嵌入变量,这是模板字符串最常见的用法

1
2
let name = 'tom'
console.log(`my name is ${name}`)//输出'my name is tom'

在模板字符串中,会保留字符串中的所有空白字符,包括空格、制表符(\t)和换行符(\n)。

这种特性,使得模板字符串非常适合用于生成多行文本或格式化的字符串内容。这与普通字符串(使用单引号 ' 或双引号 ")不同,普通字符串不会自动保留换行和缩进,必须手动添加换行符(\n)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 普通字符串
let str1 = 'Hello\nWorld';
console.log(str1);
// 输出:
// Hello
// World

// 模板字符串
let str2 = `
Hello
World
`;
console.log(str2);
// 输出:
// Hello
// World
let str3 = 'Hello
World'; //报错,不能在普通字符串中直接使用换行,必须使用换行符

在js中,字符串是不可变的,意思是一旦创建,它们的值就不能变了。因为虽然字符串是基本数据类型,但实际存储在堆中,栈中保存的是引用。

1
2
3
4
5
6
7
let lang = "Java";//这行代码,会在内存中创建一个包含 "Java" 的字符串对象,并将引用赋值给变量 lang。

//从内存中读取 lang 当前所指向的字符串 "Java"。
//将 "Java" 和 "Script" 拼接成新的字符串 "JavaScript",并在内存中创建一个新的字符串对象。
//将 lang 变量存储的引用,更新为新创建的字符串 "JavaScript"的引用
lang = lang + "Script";
console.log(lang)

字符串比较

场景== 结果=== 结果原因
2个原始字符串:let a = ‘123’, b=’123’truetruea,b都是基本数据类型中的字符串,又因为引擎会将相同的字符串字面量(如 '123')存储为同一个堆内存地址,而非创建多个实例。这样可以节省内存并提高性能。因此,ab 实际上指向了同一个堆内存地址,所以a,b的值也是相同的,因此 a === b 返回 true
原始字符串 vs String 对象:let a = ‘123’, b=new String(‘123’)truefalse===比较的结果为false,因为a,b不是同一数据类型,a的类型是string,b的类型是object;==比较的结果是true,是因为b.valueof的值就是,字面量字符串123的引用。
两个 String 对象:let a = new String(‘123’), b=new String(‘123’)falsefalsea,b的数据类型虽然相同,值也相同,但是由于对象之间的比较,比较的是引用,无论是严格比较还是非严格比较,a,b是2个不同的对象,所以a,b存储的引用并不相同,严格比较和非严格比较的值都是false

Boolean

Boolean(布尔值)类型有两个字面值: truefalse

通过Boolean可以将其他类型的数据,显式转化成布尔值

数据类型转换为 true 的值转换为 false 的值
String非空字符串“”
Number非零数值(包括负数)0 、 NaN
Object任意对象null
UndefinedN/A (不存在)undefined

Symbol

Symbol关键字的主要用途,是用来创造一个唯一的标识符,用作对象属性,确保不会产生属性冲突

1
2
3
let genericSymbol = Symbol();
let otherGenericSymbol = Symbol();
console.log(genericSymbol == otherGenericSymbol); // false

传入符号主要为了标识,符号相同并不代表值也相同

1
2
3
let fooSymbol = Symbol('foo');
let otherFooSymbol = Symbol('foo');
console.log(fooSymbol == otherFooSymbol); // false

可枚举性

Symbol类型的键默认是可枚举的,通过对象字面量或常规赋值添加的属性,默认都是可枚举的

1
2
3
const sym = Symbol();
const obj = { [sym]: 'value' };
console.log(Object.getOwnPropertyDescriptor(obj, sym).enumerable); // true

通过Object.defineProperty定义的属性,其可枚举性才默认为false,无论是字符串键还是 Symbol 键,均可通过 Object.defineProperty() 显式设置 enumerable: true/false,顾名思义,这个方法就是用来定义,修改属性的,而且每次只能修改一个属性。

1
2
3
4
5
6
7
//第一个参数指明要修改哪个对象,第二个参数指明要修改这个对象的哪个属性,第三个参数指明如何修改这个属性
Object.defineProperty(obj, 'hidden', {
value: '秘密',
enumerable: true
writable: false //是否可以被修改
configurable: false //是否可以被删除
});

在某些方法中不被考虑

Symbol 类型的键,并且不会出现在for...in 循环中,也不会被Object.keys()方法返回,因为这两种方法只考虑字符串类型的键,并不是Symbo类型的属性就是不可枚举的。

1
2
3
4
5
6
7
8
9
10
11
12
// 创建一个 Symbol
const sym = Symbol('description');
// 使用 Symbol 作为对象的键
const obj = {
normalKey: 'value for normal key',
[sym]: '123'
};
// 遍历对象属性,Symbol 键不会出现
for (let key in obj) {
console.log(key); // 输出: normalKey
}
console.log(Object.keys(obj));//输出['normalKey']

Symbol 类型的键和值,都不会包含在序列化的结果中,因为无法转化成字符串,而且JSON规范明确要求了键必须是字符串。

1
2
3
4
5
6
7
8
9
// 创建一个 Symbol
const sym = Symbol('description');
// 使用 Symbol 作为对象的键
const obj = {
name: 'tom',
normalKey: sym
[sym]: '123'
};
console.log(JSON.stringify(obj))//{"name":"tom"}

Object.assign

Object.assign会把Symbol 类型的键也拷贝进,因为Symbol类型的键默认也是可枚举的

1
2
3
4
5
6
7
8
9
10
// 创建一个 Symbol
const sym = Symbol('description');
// 使用 Symbol 作为对象的键
const obj = {
normalKey: 'value for normal key',
[sym]: '123'
};
const obj2 = {}
Object.assign(obj2, obj)
console.log(obj2)//{normalKey: 'value for normal key', Symbol(description): '123'}

Null

Null类型同样只有一个值,即特殊值 null

null明明是基本数据类型,typeof null返回的结果却是"object",逻辑上讲, null 值表示一个空对象,这也是给typeof传一个 null 会返回 "object" 的原因,这也是js的历史遗留问题。

1
2
3
let car = null;
console.log(typeof car); // "object"
null instanceof Object;// 输出false,因为null是一个特殊的基本数据类型,不代表任何对象。

Undefined

Undefined 类型只有一个值,就是特殊值 undefined,如果一个变量声明了但是未被赋值,那么这个变量的值就是undefined。

1
2
3
let message; // 这个变量被声明了,只是值为 undefined
console.log(message); // "undefined"
console.log(age); // 没有声明过这个变量,报错

引用数据类型

引用数据类型有多种,引用数据类型统称为Object,所以一般不会问有几种 ,一般只问基本类型有几种。

引用数据类型主要包括以下三种:

Array

js数组是一组有序的数据,但跟其他语言不同的是,数组中每个槽位可以存储任意类型的数据。并且,数组也是动态大小的,会随着数据添加而自动增长。

通常通过字面量表示法创建数组

1
2
let colors = ["red", 2, {age: 20 }]
colors.push(2)

或者通过Array来创建数组,给数组分配大小固定,连续的空间,内部默认没有元素;可以调用数组的fill方法填充数组,比如arr.fill(0)

1
2
3
4
const arr = new Array(4)
arr.forEach(i => { console.log(i) })//不会输出任何内容,因为数组中一个元素都没有,全都是空槽,空槽不参与遍历
arr.fill(0)
arr.forEach(i => { console.log(i) })//输出四个0

虽然说数组大小好像是固定的,比如这里初始化长度为4,但是还是可以往数组中加入元素,改变数组的大小,不过新加入的元素放在已分配空间之后

创建二维数组

1
2
3
const arr = new Array(4)//创建一个长度为4的数组,虽然创建的时候指定了长度,但是长度还是可以变化的
arr.fill(0) // 初始化/填充 数组
arr.map(i=>new Array(4).fill(0))//每次迭代都会创建一个新的数组并返回,确保二维数组中的每个数组存储空间互不干扰

上述代码可简写为:

1
const arr = new Array(4).fill(0).map( ele => new Array(4).fill(0))

可以看出在js中创建二维数组还是挺麻烦的。

Function

函数实际上是对象,每个函数都是 Function类型的实例,而 Function也有属性和方法,跟其他引用类型一样,其中最常见的属性比如prototype。但是函数和其他对象不同的是,对一个函数使用typeof返回的是function,对一个数组使用typeof返回的都是object

其他类型

除了上述说的2种之外,还包括DateRegExpMapSet等,他们都是Object类型的子类

区别

  • 对于基本类型变量,变量和值都直接存储在栈内存中;对于引用类型变量,变量名和引用都存储在栈内存中,存储在堆内存中。

  • 当基本数据类型的值,被作为参数,传递给函数或者变量时,实际上是将该值的一个副本传给了它们。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    //函数内部对参数所做的任何修改都不会影响到原始变量。
    function changeValue(x) {
    x = 10;
    }
    let a = 5;
    changeValue(a);
    console.log(a); //输出5

    let a = 10;
    let b = a; // 复制 a 的值给 b,b 是独立的新值
    b = 20;

    console.log(a); // 10(a 未受影响)
  • 当引用类型的值,被作为参数传递给函数或者变量时,实际上是将该值的一个引用传给了它们。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    let obj1 = { value: 10 };
    let obj2 = obj1; // 复制引用地址,obj2 与 obj1 指向同一个对象
    obj2.value = 20;

    console.log(obj1.value); // 20(原始对象被修改)
    console.log(obj2.value); // 20

    function modifyObject(obj) {
    obj.value = 100; // 修改共享对象的属性
    }

    let myObj = { value: 10 };
    modifyObject(myObj);
    console.log(myObj.value); // 100(原始对象被修改)

数组的常用方法

我们可以从增删查改,是否会修改原数组这几个角度,来给数组的常用方法归类

  • push():可以传入任意个数的元素,这些元素会被添加到数组的末尾,返回新数组的长度,会修改原数组。

    1
    2
    3
    let colors = []; // 创建一个数组
    let count = colors.push("red", "green"); // 推入两项
    console.log(count) // 2
  • unshift():也是可以传入任意个数的元素,这些元素会被添加到数组的首部,返回新数组的长度,会修改原数组。

    1
    2
    3
    4
    let colors = new Array(); // 创建一个数组
    let count = colors.unshift("red", "green"); // 从数组开头推入两项
    console.log(count); // 2
    console.log(colors)//['red', 'green'],说明不是先推入red,后推入green,而是视为一个整体,放到了数组的头部

    这个方法很容易和数组另一个方法shift混用,后者用来删除数组首部元素。

  • splice():第一个参数传入开始位置,第二个参数(表示删除元素的个数)传入0,表示不删除元素,后续参数传入插入的元素。

    1
    2
    3
    4
    let colors = ["red", "green", "blue"];
    let removed = colors.splice(1, 0, "yellow", "orange")
    console.log(colors) // red,yellow,orange,green,blue(插入的元素从开始下标开始排序)
    console.log(removed) // [],返回包含被删除元素的数组,因为没有元素被删除所以是空数组
  • concat():首先会创建一个当前数组的副本,然后再把它的参数添加到副本末尾,最后返回这个新构建的数组,不会影响原始数组

    1
    2
    3
    4
    let colors = ["red", "green", "blue"];
    let colors2 = colors.concat("yellow", ["black", "brown"]);
    console.log(colors); // ["red", "green","blue"],可以看到原数组并没有改变
    console.log(colors2); // ["red", "green", "blue", "yellow", "black", "brown"]

  • pop():方法用于删除数组的最后一项,同时减少数组的length 值,返回被删除的项

    1
    2
    3
    4
    let colors = ["red", "green"]
    let item = colors.pop(); // 取得最后一项
    console.log(item) // green
    console.log(colors.length) // 1
  • shift():法用于删除数组的第一项,同时减少数组的length 值,返回被删除的项

    1
    2
    3
    4
    let colors = ["red", "green"]
    let item = colors.shift(); // 取得第一项
    console.log(item) // red
    console.log(colors.length) // 1
  • splice():第一个参数传入开始位置,第二个参数传入要删除元素的个数,返回包含被删除元素的数组,如果,没有任何元素被删除,则返回空数组。

    1
    2
    3
    4
    let colors = ["red", "green", "blue"];
    let removed = colors.splice(0,1); // 删除第一项
    console.log(colors); // green,blue
    console.log(removed); // ["red"],只有一个元素的数组
  • slice():本质是返回一个数组切片,并不会修改原数组,截取区间遵循左闭右开原则。

    1
    2
    3
    4
    5
    6
    let colors = ["red", "green", "blue", "yellow", "purple"];
    let colors2 = colors.slice(1);
    let colors3 = colors.slice(1, 4);
    console.log(colors) // red,green,blue,yellow,purple
    console.log(colors2); // green,blue,yellow,purple
    console.log(colors3); // green,blue,yellow

一般通过下标修改数组元素的值,也可以使用splice先删除元素再添加元素。

1
2
3
4
let colors = ["red", "green", "blue"];
let removed = colors.splice(1, 1, "red", "purple"); // 插入两个值,删除一个元素
console.log(colors); // [red,red,purple,blue]
console.log(removed); // [green],只有一个元素的数组

一般也是通过下标来查找数组元素。

  • indexOf():传入一个元素,返回数组中第一个与该元素相等的元素的下标,使用的是严格比较,如果数组中没有该元素,则返回-1,因为NaN不与任何数相等,所以indexOf(NaN)返回值必定为-1。其实这个方法特别语义化,indexOf(元素)意思不就是某个元素的下标吗。

    1
    2
    3
    let numbers = [1, 2, 3, 4, 5, 4, 3, 2, 1,NaN];
    numbers.indexOf(4) // 3
    console.log(numbers.indexOf(NaN)) // -1
  • includes():判断某个元素是否在数组中存在,也是严格比较,存在返回true,否则返回false。对NaN做了特殊处理,能判断是它否存在于数组中,就这一点而言,是比indexOf要强大的。

    1
    2
    3
    let numbers = [1, 2, 3, 4, 5, 4, 3, 2, 1,NaN];
    numbers.includes(4) //true
    numbers.includes(NaN) //返回true
  • find():传入一个返回值是 布尔类型的回调函数,用于判断满足某个条件的元素是否存在,存在则返回第一个符合条件的元素,不存在则返回undefined,通常用于判断对象数组中是否存在某个对象。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    const people = [
    {
    name: "Matt",
    age: 27
    },
    {
    name: "Nicholas",
    age: 29
    }
    ];
    people.find((element, index, array) => element.age < 28)// {name: "Matt", age: 27}
  • findIndex():语法和用途和find相同,不过返回的是元素的下标,未找到返回-1

排序方法

  • reverse():反转数组,会修改原数组

    1
    2
    3
    let values = [1, 2, 3, 4, 5];
    values.reverse();
    alert(values); // 5,4,3,2,1
  • sort():给数组排序,sort()方法接受一个比较函数,用于判断哪个值应该排在前面,用的是非常多,特别在算法题里

    1
    2
    3
    4
    5
    6
    7
    8
    function compare(value1, value2) {
    //return value1-value2 升序排序
    //return value2-value1 降序排序
    //value1[key]-value2[key] 根据某个属性升序排序,反之降序排序
    }
    let values = [0, 1, 5, 10, 15];
    values.sort(compare);
    alert(values);

转换方法

join():把数组中的元素拼接成一个字符串,用传入的符号连接,如果传入的符号是'',那么就是一个类似将字符数组转化成字符串的过程。显然这个方法也不会修改原数组。

1
2
3
let colors = ["red", "green", "blue"];
alert(colors.join(",")); // red,green,blue
alert(colors.join("||")); // red||green||blue

迭代方法

  • some():传入一个返回值为布尔值的回调函数,作为判断条件,如果数组中存在满足条件的元素,则该方法返回true,否则返回false。要注意千万不要把这个方法写成any,数组并没有any方法,这是Promise的静态方法。

  • every():传入一个返回值为布尔值的回调函数,作为判断条件,如果数组中每个元素都满足条件,则该方法返回true,否则返回false。注意千万不要把这个方法写成all,数组中并没有all方法,这是Promise的静态方法。

  • forEach():遍历数组中的每个元素,并执行一定操作,可以修改原数组。

    1
    2
    3
    4
    let numbers = [1, 2, 3, 4, 5, 4, 3, 2, 1];
    numbers.forEach((item, index, array) => {
    // 执行某些操作
    });
  • filter():传入一个返回值为布尔值的回调函数,作为判断条件,返回一个数组,这个数组包含所有满足这个判断条件的元素。无论原数组是否包含满足条件的元素,filter 总是会返回一个新的数组。如果没有找到任何满足条件的元素,则返回的是一个空数组 []

    1
    2
    3
    let numbers = [1, 2, 3, 4, 5, 4, 3, 2, 1];
    let filterResult = numbers.filter((item, index, array) => item > 2);
    console.log(filterResult); // 3,4,5,4,3
  • map():根据传入的回调函数和数组中的每一个元素,并返回一个新的数组。要注意的是,传入的回调函数虽然也是需要有返回值的,就如同filter,some,every,但是不同的是,传入map方法的回调函数的返回值并不是一个布尔值,而是通过每个数组元素计算得到的新的值。

    1
    2
    3
    let numbers = [1, 2, 3, 4, 5, 4, 3, 2, 1];
    let mapResult = numbers.map((item, index, array) => item * 2);
    console.log(mapResult) // 2,4,6,8,10,8,6,4,2

字符串常用方法

操作方法

concat

用于将一个或多个字符串拼接成一个新字符串,返回一个新的字符串,不会修改原来的字符串,js中的字符串是不可变的

在数组中也有这个方法哦,效果也非常相似,其实数组和字符串有很多同名的方法。

1
2
3
4
let stringValue = "hello ";
let result = stringValue.concat("world");//创建一个新的字符串
console.log(result); // "hello world"
console.log(stringValue); // "hello"

slice() substr() substring()

作用是返回字符串的切片

1
2
3
4
5
6
7
8
let stringValue = "hello world";
console.log(stringValue.slice(3)); // "lo world"
console.log(stringValue.substring(3)); // "lo world"
console.log(stringValue.slice(3, 7)); // "lo w"
console.log(stringValue.substring(3,7)); // "lo w"

console.log(stringValue.substr(3)); // "lo world"
console.log(stringValue.substr(3, 7)); // "lo worl"
  • 数组中也有slice()方法

  • 可以看出slice()substring()的用法是一致的,当传入两个参数的时候,分别表示的是截取的左右区间(左闭右开,目前就没见到过左闭右闭的情况,除了正则表达式中)

  • substr()传入两个参数时,第一个表示参数起始位置,第二个参数表示的是要截取的元素的个数

  • 当只传入一个参数,三者的效果是相同的。

indexOf() startWith() includes()

indexOf:从字符串开头去搜索传入的字符串,并返回位置(如果没找到,则返回 -1 ),数组中也有这个方法,也许因为字符串本来就可以看成字符数组。

1
2
let stringValue = "hello world";
console.log(stringValue.indexOf("o")); // 4

startWith():判断字符串是否以某个字符串开头,返回值为布尔类型。

includes():判断字符串中是否包含某个字符串,返回值是布尔类型,数组中也有这个方法。

1
2
3
4
5
let message = "foobarbaz";
console.log(message.startsWith("foo")); // true
console.log(message.startsWith("bar")); // false
console.log(message.includes("foo")); // true
console.log(message.includes("bar")); // true

由此可见,无论是数组还是字符串中,都有indexOf,includes,slice,concat方法

字符串拆分

把字符串按照指定的分割符,拆分成字符数组,特别是当传入'',即空字符的时候,是真正意义上的把字符串拆分成字符数组,不会包含空字符。

1
2
3
let str = "12+23+34"
let arr = str.split("+") // [12,23,34]
let arr2 = str.split("") //['1', '2', '+', '2', '3', '+', '3', '4']

模板匹配

提及字符串,就不得不提到模板匹配,提起模板匹配就不得不提起正则表达式,会在后面介绍。

match()

接收一个参数,可以是一个正则表达式字符串,也可以是一个RegExp对象(正则表达式对象),如果你传递一个非正则表达式对象,它会被隐式转换为正则表达式;返回值是数组。

非全局匹配(传入的正则表达未加修饰符g):只会匹配第一个符合条件的字符串片段,下面给出一个例子

1
2
3
4
let text = "cat, bat, sat, fat";
let pattern = /.at/;
let matches = text.match(pattern);
console.log(matches);

匹配成功的返回结果,是一个数组,但是这个数组并不是传统意义上的数组,因为它的键不全是数字,包含第一个匹配的字符串片段更多信息。不得不说,在js中,有的数组是真像对象,但它就是数组,有的对象也是真的像数组(伪数组),但就是对象。因为在js中,数组本质就是一个对象。

  • index 属性:匹配结果在字符串中的开始位置
  • input 属性:原始字符串

如果匹配失败则返回null

全局匹配

返回所有符合条件的字符串片段,并以数组的形式给出,例子如下:

1
2
3
4
let text = "cat, bat, sat, fat";
let pattern = /.at/;
let matches = text.match(pattern);
console.log(matches);

匹配成功的返回结果,只包含符合条件的字符串片段

如果匹配失败则返回null

search()

1
str.search(regexp)
  • str 是要进行搜索操作的字符串。
  • regexp 是一个正则表达式对象。如果你传递一个非正则表达式对象(例如,一个字符串),它将被隐式转换为一个正则表达式对象。
  • 如果找到匹配项,search() 方法返回第一个匹配项首字符的下标。
  • 如果没有找到匹配项,search() 方法返回 -1
  • 是否给传入的正则表达式添加修饰符g,对结果没有影响。
  • 简单的来说search返回的就是第一个被匹配的字符串片段的下标

下面举个例子:

1
2
3
4
5
6
let text = "cas, bat, sat, fat";
let pattern = /.at/;
let pattern2 = /.at/g;
let index = text.search(pattern);
let index2 = text.search(pattern);
console.log(index,index2); //输出5,5

replace()

replace() 方法用于在字符串中查找匹配的子字符串,并用新的子字符串替换它,这个方法不会改变原始字符串,因为JavaScript中的字符串是不可变的,它会返回一个新的字符串作为结果。

1
str.replace(regexp|substr, newSubstr|function)
  • regexp (正则表达式):一个RegExp对象或者其字面量,标识要查找的子字符串。全局搜索需要使用g标志。
  • substr (字符串):将被替换的子字符串。
  • newSubstr (字符串):新子字符串,用于替换匹配项的字符串。
  • function (函数):用于创建新子字符串的函数,所以要有返回值,该函数将被每一个匹配项调用。
1
2
3
4
let str = "Hello world!";
let newStr = str.replace("world", "JavaScript");

console.log(newStr); // 输出: "Hello JavaScript!"
1
2
3
4
let str = "Hello world! Welcome to the world of programming.";
let newStr = str.replace(/world/g, "JavaScript");

console.log(newStr); // 输出: "Hello JavaScript! Welcome to the JavaScript of programming."
1
2
3
4
let str = "Hello World! Welcome to the world of Programming.";
let newStr = str.replace(/world/gi, "JavaScript");

console.log(newStr); // 输出: "Hello JavaScript! Welcome to the JavaScript of Programming."
1
2
3
4
5
let str = "20 apples, 15 bananas, and 3 cherries";
let newStr = str.replace(/\d+/g, function(number) {// \d+ 是匹配一个或多个数字的正则表达式
return number * 2; // number是被匹配的匹配项,在此处分别是20,15,3
});
console.log(newStr); // 输出: "40 apples, 30 bananas, and 6 cherries"

区别

match方法返回的是一个数组(无论是否是全局匹配),search方法返回的是下标,replace方法返回的是修改后的字符串。

说说js中的日期对象Date

JavaScript中的Date对象用于处理日期和时间。它提供了一系列方法来获取和设置日期的各个方面,如年、月、日、小时、分钟、秒和毫秒等

创建日期对象

我们都知道,通过new Date()就可以创建一个代表当前日期时间的对象,但是你有没有想过,可以给Date构造函数传入不同的参数呢?

  • 不带参数:创建一个代表当前日期和时间的对象。

    1
    2
    const now = new Date();
    console.log(now); // 输出类似 "2025-03-07T03:48:32.123Z" 的字符串(具体时间取决于执行时刻)
  • 日期字符串参数:根据提供的日期字符串,创建对应的日期对象,但通常情况下,要我们手动传入一个格式规范的日期字符串,是比较难的吧。

    1
    2
    const dateStr = new Date('2025-03-07T00:00:00');
    console.log(dateStr); // 输出 "2025-03-07T00:00:00.000Z"
  • 带时间戳参数:根据传入的时间戳,返回对应的时间日期对象

    1
    2
    3
    4
    const timestamp = new Date(1709756400000);
    console.log(timestamp); // 输出 "2025-03-07T00:00:00.000Z"
    //我们只要再调用toLocaleString方法,时间格式就变得熟悉了
    console.log(timestamp.toLocaleString()) // 2024/3/7 04:20:00
  • 通过多个数值参数创建:指定年、月(从0开始计数)、日、时、分、秒和毫秒,感觉是比传入一个日期字符串好用?

    1
    2
    const customDate = new Date(2025, 2, 7, 0, 0, 0, 0); // 注意月份是从0开始计数的,所以2表示3月
    console.log(customDate); // 输出 "2025-03-07T00:00:00.000Z"

获取日期信息

Date对象提供了多种方法来获取日期的不同部分:

  • getFullYear():获取四位数的年份。
  • getMonth():获取月份(0-11)。
  • getDate():获取一个月中的某一天(1-31)。
  • getDay():获取星期几(0-6,0表示星期天)。
  • getHours():获取小时(0-23)。
  • getMinutes():获取分钟(0-59)。
  • getSeconds():获取秒(0-59)。
  • getMilliseconds():获取毫秒(0-999)。
  • getTime():获取自1970年1月1日以来的毫秒数,也就是时间戳,获取时间戳的方法还有Date.now()

其他常用方法

  • toDateString():返回日期部分的字符串表示形式,不常用
  • toTimeString():返回时间部分的字符串表示形式,不常用
  • toISOString():返回ISO格式的日期字符串(UTC时间),不常用
  • toLocaleString():基于本地时间格式化日期和时间,常用,要注意的是不要把locale(现场)写成local(本地)
  • toLocaleDateString():仅格式化日期部分为本地格式。
  • toLocaleTimeString():仅格式化时间部分为本地格式。
1
2
3
4
5
6
7
const date = new Date()
console.log(date.toDateString())//Fri Mar 07 2025
console.log(date.toTimeString())//2:38:26 GMT+0800 (中国标准时间)
console.log(date.toISOString())//2025-03-07T04:38:26.053Z
console.log(date.toLocaleString())//2025/3/7 12:41:03
console.log(date.toLocalDateString())//2025/3/7
console.log(date.toLocaleTimeString())//12:41:03

说说js中的正则表达式

创建正则表达式

字面量语法

1
const regex = /pattern/;

例如:/ab+c/i 匹配 “abc”, “ABBC” 等。

构造函数语法

1
const regex = new RegExp('pattern', '修饰符');

例如:new RegExp('ab+c', 'i'),等价于/ab+c/i,感觉还是字面量语法方便啊

正则表达式语法

元字符

  • \d:数字(0-9),因为digit的意思就是数字的意思;
  • \D:非数字
  • \w:单词字符(字母、数字、下划线,在js中的标识符,就是由这三者构成的),word就是单词的意思;
  • \W:非单词字符
  • \s:空白符(空格、制表符、换行),space就是空格的意思,然而\s匹配的是所有类型的空白字符,而不仅仅是空格。
  • \S:非空白符
  • .:匹配除换行外任意字符(若需包含换行,使用修饰符 s
  • ^:字符串开头;$:字符串结尾

字符组

  • [abc]:匹配 a、b、c 中的任意一个
  • [a-z]:匹配 a 到 z 的任意小写字母
  • [^abc]:匹配任何一个不在 abc 范围内的字符,也就是说**^在字符组中也有取反的意思**

要注意的是,无论字符组中有多少个符号,匹配的都只是一个字符

分组/捕获组

小括号可以将多个字符或子表达式组合在一起,形成一个逻辑单元

1
2
3
const pattern = /(ab)+/;
const str = "ababab";
console.log(str.match(pattern)); // ["ababab", "ab"]
  • (ab)ab 视为一个整体,而不是一个单独的字符。
  • + 表示匹配这个整体一次或多次。
  • 匹配结果是整个字符串 "ababab",而第一个捕获组的结果是 "ab"

小括号会创建一个捕获组,用于提取匹配的部分内容。每个捕获组的内容可以通过 match() 方法的返回值中的数组访问。

1
2
3
4
5
6
7
8
const pattern = /(\d{4})-(\d{2})-(\d{2})/;
const str = "2023-10-05";
const match = str.match(pattern);

console.log(match[0]); // "2023-10-05"(完整匹配)
console.log(match[1]); // "2023"(第一个捕获组)
console.log(match[2]); // "10"(第二个捕获组)
console.log(match[3]); // "05"(第三个捕获组)、

其实上述正则表达式不使用捕获组,也能匹配到str,但如果不使用捕获组,结果数组中也就不会有捕获组。

量词

  • *:0 次或多次
  • +:1 次或多次
  • ?:0 次或 1 次
  • {n}:精确匹配 n 次
  • {n,}>=n
  • {n,m}:n 到 m 次,左闭右闭

总结:在js中的正则表达式中,中括号表示字符组,只匹配一个字符;大括号表示量词,表示匹配多少次,而小括号则表示一个分组或者匹配组。

修饰符

  • i:不区分大小写
  • g:全局匹配(查找所有匹配项)
  • m:多行模式(^$ 匹配每行的开头和结尾)
  • s:dotAll 模式(. 匹配换行符)
  • u:Unicode 模式
  • y:粘性匹配(从 lastIndex 开始匹配)

常用方法

test()

1
/hello/.test('hello world'); // true

回布尔值,判断是否匹配成功

exec()

1
/(\d+).(\d+)/.exec('3.14'); // ['3.14', '3', '14', index: 0, ...]
  • 不使用全局标志的时候

    调用一个正则表达式的exec方法,并传入一个字符串,效果完全等同于:调用一个字符串的match方法并传入一个正则表达式

  • 使用全局标志

    每次调用 exec() 都会从上一次匹配结束的位置,开始寻找下一个匹配项,这一点字符串的match方法就不同了,真难记啊。

Object的常见静态方法

Object.keys()

Object.keys() 方法,用于返回一个对象自身,可枚举属性,且不包括Symbol类型的属性,组成的数组。

1
2
const obj = { a: 1, b: 2, c: 3, [Symbol()]: 4 };
console.log(Object.keys(obj));// 输出: ['a', 'b', 'c']
1
2
3
4
5
6
const obj = {};
Object.defineProperty(obj, 'invisible', {
value: 'hidden',
enumerable: false // 设置为不可枚举
});
console.log(Object.keys(obj)); // 输出: []

我们还经常使用for in来获取一个对象所有的可枚举属性,但是它与Object.keys() 方法不同的是,还能获取原型链上的可枚举属性,所以在某些情况还需要借助hasOwnProperty来判断是不是自身的属性。

Object.values()

Object.values() 方法,用于返回一个对象自身可枚举属性(且不包括Symbol类型的属性)的值组成的数组。它只返回对象自身的属性(不包括原型链上的属性),并且这些属性必须是可枚举的。

1
2
const obj = { a: 1, b: 2, c: 3, [Symbol()]: 4 };
console.log(Object.values(obj));// 输出: [1, 2, 3]

Object.entries()

Object.entries() 方法,用于返回一个对象自身可枚举属性(且不包括Symbol类型的属性)键值对数组。每个键值对是一个包含两个元素的数组:第一个元素是属性名(键),第二个元素是对应的属性值。

1
2
const obj = { a: 1, b: 2, c: 3, [Symbol()]: 4 };
console.log(Object.entries(obj))// 输出: [['a', 1], ['b', 2], ['c', 3]]

Object.assign()

传入2个对象作为参数,会将第二个对象中可枚举的自有属性(包括Symbol类型的属性),拷贝到第一个对象。返回值就是传入的第一个对象,传入的第一个对象会被修改

1
2
3
4
5
6
7
8
9
10
11
12
var obj = {
age: 18,
nature: ['smart', 'good'],
names: {
name1: 'tom',
},
[Symbol()]:'cindy'
}
let obj2 = {}
var newObj = Object.assign(obj2, obj);
console.log(newObj)//{age: 18, nature: Array(2), names: {…}, Symbol(): 'cindy'}
console.log(obj2 == newObj) //返回true,说明返回的就是原对象(传入的第一个对象)

Object.defineProperty

Object.defineProperty 是 JavaScript 中用于定义或修改对象属性的底层方法

1
Object.defineProperty(obj, prop, descriptor);
  • obj: 要定义属性的目标对象。
  • prop: 要定义或修改的属性名称(字符串或 Symbol)。
  • descriptor: 属性描述符对象,用于定义属性的行为。descriptor 是一个对象,可以包含以下键值对:

数据描述符

属性名描述
value属性的值,默认为 undefined
writable是否可以修改属性的值,默认为 false(即不可写)。
enumerable是否可以通过 for...inObject.keys() 枚举该属性,默认为 false
configurable是否可以删除该属性或修改其描述符,默认为 false

存取器描述符

属性名描述
get定义获取属性值时调用的函数,默认为 undefined
set定义设置属性值时调用的函数,默认为 undefined
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const obj = {};
let _age = 25; // 私有变量

Object.defineProperty(obj, 'age', {
get() {
return _age;
},
set(newValue) {
if (newValue < 0) {
console.error('Age cannot be negative');
} else {
_age = newValue;
}
},
enumerable: true,
configurable: true
});

console.log(obj.age); // 输出 25
obj.age = 30; // 设置新值
console.log(obj.age); // 输出 30
obj.age = -5; // 触发错误提示,但不会修改值
console.log(obj.age); // 输出 30

typeof和instanceof

typeof

typeof 操作符返回一个字符串,表示值的数据类型。

1
2
typeof operand //这种方式用的多
typeof(operand)

这两种使用方法都是可以的。下面是一些例子。

1
2
3
4
5
6
7
8
9
10
typeof 1 // 'number'
typeof '1' // 'string'
typeof undefined // 'undefined',返回对应的基本类型
typeof true // 'boolean',返回对应的基本类型
typeof Symbol() // 'symbol',返回对应的基本类型
typeof null // 'object',这个比较特别
typeof [] // 'object',返回的不是array
typeof {} // 'object'
typeof console // 'object'
typeof console.log // 'function'

值得注意的是,对所有引用数据类型(除了function,包括数组,普通对象),使用typeof返回的都是object

instanceof

主要用来判断某个构造函数,是否在某个实例对象的原型链上。

1
object instanceof constructor

区别

  • typeof返回的是字符串,instanceof返回的是布尔值

  • typeof只能能准确判断基本数据的类型,不能准确判断引用数据的类型。

  • intanceof只能准确判断引用数据的类型,不能判断基本数据的类型

可以看到,上述两种方法都有弊端,并不能满足所有场景的需求。

谈谈 JavaScript 中的类型转换机制

前面我们讲到,JS中有六种简单数据类型:undefinednullbooleanstringnumbersymbol,以及引用类型:object

常见的类型转换有:强制转换(显示转换),自动转换(隐式转换)

显式转换

显式转换,即我们很清楚可以看到这里发生了类型的转变,常见的方法有:Number(),parseInt(),String(),Boolean()

Number()

将任意类型的值转化为数值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Number(324) // 324
// 字符串:如果可以被解析为数值,则转换为相应的数值
Number('324') // 324
// 字符串:如果不可以被解析为数值,返回 NaN
Number('324abc') // NaN
// 空字符串转为0,空数组也转换成0
Number('') // 0
// 布尔值:true 转成 1,false 转成 0
Number(true) // 1
Number(false) // 0
// undefined:转成 NaN
Number(undefined) // NaN
// null:转成0,这就是undefined和null的区别
Number(null) // 0
//对象:通常转换成NaN,除了只包含单个数值的数组和空数组
Number({}) // NaN
Number({a: 1}) // NaN
Number([1, 2, 3]) // NaN
Number([5]) // 5
Number([]) // 0
  • 从上面可以看到,Number转换的时候是很严格的,只要有一个字符无法转成数值,整个字符串就会被转为NaN
  • null转化成数字类型是0,而undefined转化成数字类型是NaN,这是二者最大的区别之一
  • 总结一下,哪些数据转化成数字类型后的值是0:
    • 空字符串
    • false
    • null
    • 空数组

parseInt()

parseInt相比Number,就没那么严格了,parseInt函数逐个解析字符,遇到不能转换的字符就停下来。这个方法是可以直接被调用的,不许要显式借助其他对象

1
parseInt('32a3') //32

要注意的是,如果传入parseInt的值不是以数字开头的字符串,那么parseInt的返回值将是NaN ,这一点其实是和Number()相同的。

1
2
console.log(parseInt(true))//输出NaN,因为传入的不是字符串
console.log(parseInt('a123'))//输出NaN

parseInt方法类似的还有parseFloat方法,后者和前者不同的是,是从字符串中提取出浮点数。

String()

可以将任意类型的值转化成字符串

1
2
3
4
5
6
7
8
9
10
11
12
13
// 数值:转为相应的字符串
String(1) // "1"
//字符串:转换后还是原来的值
String("a") // "a"
//布尔值:true转为字符串"true",false转为字符串"false"
String(true) // "true"
//undefined:转为字符串"undefined"
String(undefined) // "undefined"
//null:转为字符串"null"
String(null) // "null"
//对象
String({a: 1}) // "[object Object]"
String([1, 2, 3]) // "1,2,3" 调用数组的toString方法就是去除左右括号

可以看到,对于基本数据类型,强制转化成字符串,就是加个双引号就好了,而引用数据类型就不一样了,需要调用toString方法

Boolean()

可以将任意类型的值转为布尔值,转换规则如下:

1
2
3
4
5
6
7
8
Boolean(undefined) // false
Boolean(null) // false
Boolean(0) // false
Boolean(NaN) // false
Boolean('') // false
Boolean({}) // true
Boolean([]) // true
Boolean(new Boolean(false)) // 因为返回的是一个对象Boolean {false},所以转化成布尔值是true

隐式转换

隐式转换本质就是偷偷帮我们调用了显式转换的函数,在隐式转换中,我们可能最大的疑惑是 :何时发生隐式转换

我们这里可以归纳为两种情况发生隐式转换的场景:

  • 比较运算(==!=><
  • 算术运算(+-*/%
  • ifwhile需要布尔值地方

除了上面的场景,还要求运算符两边的操作数不是同一类型

  • 自动转化成布尔值

    在需要布尔值的地方,就会将非布尔值的参数自动转为布尔值,系统内部会调用Boolean函数

  • 自动转换成字符串

    遇到预期为字符串的地方,就会将非字符串的值自动转为字符串

    常发生在+运算中,一旦存在字符串,则会进行字符串拼接操作

    1
    2
    3
    4
    5
    6
    7
    8
    '5' + 1 // '51'
    '5' + true // "5true"
    '5' + false // "5false"
    '5' + {} // "5[object Object]" ,因为{}转化成字符串是[object Object]
    '5' + [] // "5" 因为[]转换成字符串是空串
    '5' + function (){} // "5function (){}",因为函数调用toString方法返回值是function (){}
    '5' + undefined // "5undefined"
    '5' + null // "5null"

    对于基本数据类型和函数,字符串拼接的时候直接参与拼接,对于其他引用数据类型,需要先调用toString方法。哈哈,原来字符串凭借不是所有情况都是直接拼接啊。

  • 自动转换成数值

    除了左右两边包含字符串的+号,其他运算符都会把参与运算的数据自动转成数值

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    '5' - '2' // 3
    '5' * '2' // 10
    true - 1 // 0
    false - 1 // -1
    '1' - 1 // 0
    '5' * [] // 等价于5*0
    false / '5' // 等价于0/5
    'abc' - 1 // 等价于NaN-1
    null + 1 // 等价于0+1
    undefined + 1 // 等价于NaN+1

说说你对BOM的理解

BOM (Browser Object Model),浏览器对象模型,提供了独立于内容,与浏览器窗口进行交互的对象

其作用就是跟浏览器做一些交互效果,比如如何进行页面的后退,前进,刷新,浏览器的窗口发生变化,滚动条的滚动,以及获取客户的一些信息如:浏览器品牌版本,屏幕分辨率

window

Bom的核心对象是window,它表示浏览器的一个实例,locationnavigator等后续介绍的对象都是window的属性。

在浏览器中,window对象有双重角色,即是浏览器窗口的一个接口,又是全局对象,因此所有在全局作用域中声明的变量、函数都会变成window对象的属性和方法

window.scrollTo

如果有滚动条,将横向滚动条移动到相对于窗体宽度为x个像素的位置,将纵向滚动条移动到相对于窗体高度为y个像素的位置

1
window.scrollTo(0, 500);//将页面垂直滚动到距离页面顶部500像素的位置,而水平滚动条不会发生变化。

不需要添加单位

window.scrollBy

如果有滚动条,将横向滚动条向左移动x个像素,将纵向滚动条向下移动y个像素,也不需要添加单位

window.open

window.open()既可以导航到一个特定的url,也可以打开一个新的浏览器窗口。

window.open() 会返回新窗口的引用,也就是新窗口的 window 对象,当使用 window.open() 方法打开新窗口时,如果返回值是 null这通常意味着浏览器阻止了该弹窗的创建。现代浏览器为了防止恶意网站滥用弹窗,通常会限制非用户交互触发的弹窗。如果你在页面加载时或没有明确的用户动作(如点击事件)的情况下调用 window.open(),浏览器可能会认为这是未经请求的弹窗,并阻止它。

比如直接在script标签中书写:

1
window.open('sanye.blog')//被浏览器阻止
1
2
3
document.querySelector('button').addEventListener('click', (e) => {
myWin = window.open('http://www.vue3js.cn', '_blank')
})//可行
1
var newWindow = window.open(url, target, features[, replace]);

参数分析:

url :类型为String,新窗口要加载的 URL 地址。如果省略或设置为 null,则会打开一个空白窗口。

target :类型为 String,指定新窗口的目标位置。它可以是以下常用的预定义值之一:

  • _self: 在当前页面中加载新页面(默认行为)。
  • _blank: 在新的窗口或标签页中加载页面。

features:类型为String,是一系列用逗号分隔的字符串,用于指定新窗口的各种属性和行为。每个特征可以带有或不带参数

  • width = 600: 设置窗口宽度为 600 像素。
  • height = 400: 设置窗口高度为 400 像素

replace:类型为Boolean, 如果设置为 true,则新加载的页面将替换历史记录中的当前条目;如果为 false 或未提供,则会在历史记录中添加一个新条目。这对于防止用户多次点击后退按钮返回到同一个页面非常有用。

window.close

仅用于关闭通过 window.open() 打开的窗口,如果尝试关闭一个不同域名下的窗口,可能会遇到跨域限制。在这种情况下,window.close() 可能不会工作,因为浏览器的安全模型会阻止你操作不属于同一源的窗口。

1
myWin.close()//关闭myWin窗口,它是使用 `window.open()` 打开的新窗口

新创建的 window 对象有一个 opener 属性,该属性指向打开他的原始窗口对象。

location

是一个对象,包含了许多属性,一个url地址例子如下:

1
http://www.wrox.com:80/WileyCDA/?q=javascript#contents

可以将location理解为当前页面URL的JS抽象

属性

location属性描述如下:

属性名例子说明
hash“#contents”url中,#后面的字符,没有则返回空串
hostwww.wrox.com:80服务器名称和端口号
hostnamewww.wrox.com域名,不带端口号
hrefhttp://www.wrox.com:80/WileyCDA/?q=javascript#contents完整url
pathname“/WileyCDA/“服务器下面的文件路径
port80url的端口号,没有则为空
protocolhttp:使用的协议
search?q=javascripturl的查询字符串,通常为?后面的内容

除了 hash之外,只要修改location的一个属性,就会导致页面重新加载新URL,因为hash值不会发送给服务器,所以修改哈希值后刷新也面也没意义。

location.reload

此方法可以重新刷新当前页面。这个方法会根据最有效的方式刷新页面,如果页面自上一次请求以来没有改变过,页面就会从浏览器缓存中重新加载,这一点和浏览器的缓存策略相关。如果要强制从服务器中重新加载,传递一个参数true即可。

location.replace

1
2
3
4
5
// 跳转到新页面,但在历史记录中不保留当前页面
location.replace("https://www.example.com");

// 简写(window 可省略)
location.replace("/new-page.html");

效果:浏览器立即跳转到指定 URL,当前页面不会出现在“后退”历史中。可以防止用户点击“后退”回到登录页(避免重复提交或自动填充密码),提升用户体验。

history

history是window对象的一个属性,它本身也是个对象,提供了许多api,主要用来操作浏览器URL的历史记录,允许我们编程式控制页面在历史记录之间跳转,也允许我们修改历史记录。检查一个页面并在控制台输入history,即可查看当前页面的statescrollRestoraion等信息。

API作用
history.back()跳转到前一个页面,如果没有前一个页面,则不做响应,不会改变history.length
history.forward()跳转到后一个页面,如果当前就最新页面,则不做响应,不会改变history.length
history.go()传入数字,正数表示前进几个页面,负数表示后退几个页面,0表示刷新页面,不会改变history.length
history.length历史记录栈的长度,它是一个只读属性,无法直接修改。
history.pushState()历史记录栈顶添加一条记录,历史记录条数加1,但是不会跳转页面。在当前页面调用这个api,你能明显的看到url改变了,但是页面没有跳转。接收三个参数:历史记录对象(state),页面标题,URL路径。
history.replaceState()不会增加历史记录数目,会修改当前历史记录
history.state访问当前页面的状态对象。
history.scrollRestoraion如果值为auto,则在前进或者后退的时候,滚动条会回到原来的位置。如果值为manual(手动的),则不会恢复。默认值是auto,即后退到历史页面的时候,滚动条会回到原来的位置。可以通过在页面(html文件)内部的js代码中使用history.scrollRestoraion来修改这个页面滚动条的恢复方式,要注意的是,这么做只会影响当前页面的滚动行为,不会影响整个页面栈中所有页面的滚动行为

历史记录用一个栈来维护,每添加一历史记录的操作可以叫做push(入栈),当前页面就是历史记录栈顶的页面 ;假设当前历史记录栈的大小是3,当执行history.back(),弹出(pop)一条历史记录,页面也随之发生变化,因为栈顶元素改变了,但是这条历史记录并不会丢失,当我们执行history.forward(),它又会重新成为历史记录栈的栈顶元素。

navigator 对象主要用来获取浏览器的属性,区分浏览器类型。属性较多,且兼容性比较复杂。

screen

保存的纯粹是客户端能力信息,也就是浏览器窗口外面的客户端显示器的信息,比如像素宽度和像素高度。

DOM常见的操作有哪些

DOM是什么

浏览器根据html标签生成的js对象,所有的标签属性都可以在上面找到(所以说node中没有dom),修改这个对象属性会自动映射到标签上。关键词:浏览器,html标签,js对象,属性映射。

nodeType

nodeType 是一个只读属性,用于标识 DOM 节点的类型。常见的nodeType值包括:

  • 1:元素节点(Element),因为元素结点是最常见的结点,所以nodeType的值是1
  • 3:文本节点(Text
  • 8:注释节点(Comment
  • 9:文档节点(Document

DOM常见的操作

创建节点

createElement

创建元素结点

1
const divEl = document.createElement("div");

createTextNode

创建文本结点

1
const textEl = document.createTextNode("content");

获取节点

可以通过捕获的方式获取dom结点,也可以通过一个dom结点的属性来获取另一个dom结点

querySelector

传入任何有效的css 选择器,即获得首个符合条件的Dom元素:

1
2
3
4
5
document.querySelector('.element')
document.querySelector('#element')
document.querySelector('div')
document.querySelector('[name=username]')
document.querySelector('div + p > span')

如果页面上没有指定的元素时,返回 null

querySelectorAll

传入任何有效的css 选择器,返回一个伪数组,包含全部符合匹配条件的DOM元素。

1
const notLive = document.querySelectorAll("p");

其他方法

1
2
3
4
5
6
7
8
9
document.getElementById('id属性值');//返回拥有指定id的对象的引用
document.getElementsByClassName('class属性值');//返回拥有指定class的对象集合
document.getElementsByTagName('标签名');//返回拥有指定标签名的对象集合
document.getElementsByName('name属性值'); //返回拥有指定名称的对象结合
document/element.querySelector('CSS选择器'); //仅返回第一个匹配的元素
document/element.querySelectorAll('CSS选择器'); //返回所有匹配的元素
document.documentElement; //获取页面中的HTML标签
document.body; //获取页面中的BODY标签
document.all; //获取页面中的所有元素节点的对象集合型,不是标准DOM的一部分,它的行为可能在不同的浏览器中表现不同

我们仅通过观察是...Element...还是,...Elements...就能判断出返回的结果是集合还是单独的元素

除此之外,每个DOM元素还有parentNodechildNodesfirstChildlastChildnextSiblingpreviousSibling属性,关系图如下图所示。

parentNode和parentElement

parentNode 返回指定节点的父节点,这个父节点可以是任何类型的节点,包括文档类型节点、元素节点、文本节点等。但是,在实际应用中,除了元素节点外,其他类型的节点很少作为父节点存在。

parentElement 仅返回指定节点的父元素节点(即类型为HTMLElement的节点)。如果指定节点的父节点不是一个元素节点(例如,它可能是一个文本节点),则 parentElement 返回 null,即先捕获再判断类型。

简单的来说,就是一个对父节点的类型有要求,一个没有。

childNodes和children

childNodes 返回一个实时的NodeList对象,包含了指定节点的所有直接子节点(一级子节点),包括元素节点、文本节点、注释节点等所有类型的节点。

children 返回一个实时的 HTMLCollection 对象,只包含指定节点的直接子元素节点(一级元素结点,即标签)。不包括文本节点、注释节点等其他类型的节点。

previousElementSibling和previousSibling

前者用来获取上一个元素节点(即 HTML 标签),后者用来获取上一个任意类型的结点

更新结点

innerHTML

不但可以修改一个DOM节点的文本内容,如果传入的是html片段,还会被解析成dom结点。

1
2
3
4
5
6
// 获取<p id="p">...</p >
var p = document.getElementById('p');
// 设置文本为abc:
p.innerHTML = 'ABC'; // <p id="p">ABC</p >
// 设置HTML:
p.innerHTML = 'ABC <span style="color:red">RED</span> XYZ';

innerText、textContent

自动对字符串进行HTML编码,就是把小于号转化成&lt;大于号转化成&gt;保证无法设置任何HTML标签

1
2
3
4
5
6
// 获取<p id="p-id">...</p >
var p = document.getElementById('p-id');
// 设置文本:
p.innerText = '<script>alert("Hi")</script>';
// HTML被自动编码,无法设置一个<script>节点:
// <p id="p-id">&lt;script&gt;alert("Hi")&lt;/script&gt;</p >

两者的区别在于读取属性时,innerText不返回隐藏元素的文本(即 display: none 或者 visibility: hidden),而textContent返回所有文本

1
2
3
4
5
6
7
8
9
10
<div id="example">
<p>可见文本</p>
<p style="display: none;">隐藏文本</p>
</div>

<script>
const element = document.getElementById('example');
console.log(element.innerText); // 输出: "可见文本"
console.log(element.textContent); // 输出: "可见文本\n隐藏文本"
</script>

添加结点

appendChild

把一个节点添加到父节点的最后一个子节点之后,如果这个添加的结点已经在页面中存在,那么这个结点会先从原位置删除

1
2
3
4
5
6
<p id="js">JavaScript</p >
<div id="list">
<p id="java">Java</p >
<p id="python">Python</p >
<p id="scheme">Scheme</p >
</div>

添加一个p元素

1
2
3
4
const js = document.getElementById('js')
js.innerHTML = "JavaScript"
const list = document.getElementById('list');
list.appendChild(js);

HTML结构变成了下面

1
2
3
4
5
6
<div id="list">
<p id="java">Java</p >
<p id="python">Python</p >
<p id="scheme">Scheme</p >
<p id="js">JavaScript</p > <!-- 添加元素 -->
</div>

insertBefore

1
parentElement.insertBefore(newElement, referenceElement)

子节点会插入到referenceElement之前

  • parentElement: 这是要操作的目标元素,新的子节点将被添加到这个元素的子节点列表中。
  • newElement: 这是你想要插入的新元素节点。
  • referenceElement: 这是在新元素插入之前,所依据的参考元素。新元素会被放置在这个参考元素之前。如果这个参数为 null,则新元素会被插入到父元素的最后,就像使用 appendChild() 一样。

删除结点

removeChild

删除一个节点,首先要获得该节点本身以及它的父节点,然后,调用父节点的removeChild方法,把自己删掉,也就是说,一个结点是不能删除自身的,而是需要借助父节点。

1
2
3
4
5
6
7
// 拿到待删除节点:
const self = document.getElementById('to-be-removed');
// 拿到父节点:
const parent = self.parentNode;
// 删除并返回被删除的dom元素
const removed = parent.removeChild(self);
removed === self; // true

删除后的节点虽然不在文档树中了,但其实它还在内存中,可以随时再次被添加到别的位置。

说说js中的布局属性

要明白如何实现功能,我们首先要搞清楚dom元素的一些布局属性

clientWidth,scrollWidth,offsetWidth

client系列

clientWidth/clientHeight:可视区域的宽/高+内边距(padding),不包含border

offset系列

offsetWidth/offsetHeight:可视区域的宽/高+内边距+border+滚动条,这两个属性通常被拿来与clientWidth/clientHeight属性比较。这2类属性的范畴都包含可视区域的宽高和padding,都不包含margin(明明和padding一样都是边距,为什么就这么不受待见呢),区别在于offset系列还包含border,后者不包含,范围更小。

scroll系列

scrollWidth/scrollHeight有滚动条的元素的整体的宽高。一个没有滚动条的元素,它的scrollWidth/scrollHeight属性的值等于它的clientWidth/clientHeight属性的值。

举个例子,我们在一个高度为600px的盒子box里放两个背景颜色不同,高度都是400px的盒子box1,box2,并给box添加css属性

1
overflow:auto 
1
2
3
4
5
6
7
8
9
补充一下overflow属性的值
visible(默认值):内容不会被裁剪,而是会呈现在元素框之外。
hidden:内容会被裁剪,并且超出的部分不会显示。浏览器不会为溢出内容提供任何滚动机制。
scroll:即使内容并未溢出,也提供滚动条
auto:如果内容溢出了元素框,则浏览器会根据需要提供滚动条。如果内容没有溢出,则不显示滚动条

单个方向上的溢出控制
overflow-x:控制水平方向上的溢出。
overflow-y: 控制垂直方向上的溢出。

这样box盒子就出现了滚条,可以实现内容的滚动,内部盒子也不会影响外部盒子的布局(开启了BFC,可以观察添加该条属性前后,body高度的变化,从800px变为600px)。然后我们访问box盒子(有滚动条的盒子)的clientHeight属性和scrollHeight属性

1
2
box.clientHeight //600px
box.scrollHeight //800px

这样是不是就很容易理解client和scroll之间的区别呢。对于没有滚动条的元素,clientWidth/clientHeightscrollWidth/scrollHeight的值是一一相等的。

当我们不断地给body添加元素,**body的高度总有超过浏览器窗口高度的时候,此时body标签的父元素,html标签会自动开启滚动条html.clientHeight就是浏览器窗口**的高度。

scrollLeft和scrollTop

scroll开头的属性中,还有两个重要的属性,scrollLeft/scrollTop

表示具有滚动条的元素,顶部滚动出可视区域的高度,或者左部滚动出可视区域的宽度,对于不具有滚动条的元素,这两个属性的值都是0。这两个属性是可读写的,将元素的 scrollLeftscrollTop 设置为 0,可以重置元素的滚动位置,通常用来实现一键到底,或者返回顶部等功能。

offsetLeft和offsetTop

  • 元素左部/顶部,距离最近的定位元素的距离,相对的不是视口,通常是固定的,不会随页面滚动而改变。
  • offsetLeft/offsetTop这2个属性是只读的,不能手动修改
  • 要注意的是,没有offsetRightoffsetBottom属性。

offsetX和offsetY

  • offsetXoffsetY 是与鼠标事件相关的属性,通常在处理用户交互时使用,注意要和offsetLeft/offsetTop区分开来。
  • 这两个属性提供了鼠标指针相对于触发事件的元素(即事件目标元素,可以通过event.target获得)的 X 和 Y 坐标。它们是 MouseEvent 对象的一部分
  • 我曾经尝试使用这2个属性来做放大镜的效果,发现一直实现不了,后来发现了问题所在,触发鼠标事件的目标元素始终是蒙层,而不是商品图片,可以通过给蒙层添加point-events:none忽略鼠标事件来解决问题。

说说js资源加载事件

DOMContentLoaded

DOMContentLoaded事件是在HTML文档被完全加载和解析之后触发的,也就是说,当浏览器已经解析完整个HTML文档,DOM树构建完毕,这时候才会触发这个事件。不过,可能需要注意的是,虽然DOM树已经构建完成,但像图片和样式表,这些外部资源可能还没有加载完毕

1
2
3
document.addEventListener('DOMContentLoaded', function() {
console.log('HTML 文档已加载并解析完成');
});

load

则是在所有资源(包括图片、样式表等)都加载完毕后才触发,只等待资源加载,不等待资源解析,也不等待所有的异步请求完成

比如通过 JavaScript 发起的 AJAX 请求或 Fetch API 请求,甚至不等待动态加载的内容(如通过 JavaScript 动态插入的图片或其他资源),为什么要等待呢?,load事件怎么知道你请求什么时候响应,怎么知道你什么时候插入图片或者其他资源,如果你始终不这么操作,load难不成还一直等待你?

1
2
3
4
window.addEventListener('load', function() {
console.log('页面及所有资源已完全加载');
//此时可以确认所有资源,包括图片,样式表等,都已经加载完毕
});

需要值得注意的是,首屏渲染,只需要等待html标签解析完毕,构建好dom树,等待css文件加载并解析完,生成cssom树之后,就可以进行。换句话说,首屏渲染不等待图片资源,所以有时候我们能看到页面渲染出来了,但是图片没加载出来的情况。

unload和beforeunload

当用户导航至其他页面,且新页面在本窗口打开、或者关闭当前标签页或窗口、或者刷新页面时,都会触发 unload 事件,但是我们常用的其实是beforeunload,即在页面卸载前做些什么,因为unload事件触发的时候页面已经被卸载了,我们做任何操作都没用了。

1
2
3
4
5
window.addEventListener('beforeunload', function (e) {
//阻止事件默认行为,一定要添加,光注册beforeunload事件监听没用
//这样就能阻止unload事件触发,页面就不会被卸载
e.preventDefault();
});

当用户刷新页面的时候,浏览器会提示是否刷新站点;当用户跳转到其他页面的时候,提示是否进行页面跳转。