六、JS收官
(一)环境搭建
1.安装 node
2.测试安装
- 命令终端中输入
node -v
3.运行 JS 代码
- 命令终端中进入 js 文件所在目录
node [js文件名]
4.使用 code runner 插件
- VS Code 安装 code runner 插件
(二)知识回顾
1.面向对象
- 面向对象是编程范式的一种
- 分为两大类:
- 命令式编程
- 声明式编程
1)命令式编程
- 强调的是 How
- 会告诉计算机如何实现,写明每一个步骤
- 命令式编程范式都有一个特点 —— 简单易懂
- 所以早期很长一段时间(2015、2016)都流行命令式编程
- 分类:
- 面向过程
- 面向对象
// 求数组里面所有偶数的和【面向过程】
var arr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
var sum = 0;
for (var i = 0; i < arr.length; i++) {
if (arr[i] % 2 === 0) {
sum += arr[i];
}
}
2)声明式编程
- 强调的是 What
- 会告诉计算机想要什么,不管怎么实现
- 声明式编程最大的缺点就是 —— 上手困难
- 分类:
- DSL(领域专用语言)
- CSS
- SQL
- 正则表达式
- 函数式编程
- DSL(领域专用语言)
到了 2016 年之后,慢慢就开始流行声明式编程
一个典型的例子就是 React,React 以前流行的是类组件,但是现在已经慢慢的抛弃了类组件,开始全面推广函数组件
这个变化不仅仅只是组件风格的变化,还反映出了整个 React 编程范式上面的变化
// 求数组里面所有偶数的和【函数式编程】
var arr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
var sum = arr.filter((i) => i % 2 == 0).reduce((a, b) => a + b);
声明式编程的背后,其实也是命令式编程
命令式编程:假设你要做一盘回锅肉,如果采用命令式编程的风格,那么意味着所有的步骤你要亲力亲为,从养猪、杀猪、种植蒜苗,切菜....
声明式编程:现在你不仅仅要做一盘回锅肉,你现在要做的是满汉全席(软件规模变大),没有办法所有的事情都亲力亲为,这个时候就需要有一些现成的东西(切好的猪肉、切好洗好的蒜苗、配好的调味包),你可以快速的做出更多的菜。但是切换的猪肉的背后还是用的命令式的那一套来做的
3)JS 编程范式
- JS 支持多种范式的编程模式,既可以采用 面向对象 的编程范式,也可以采用 函数式编程 的编程范式
- 关于 JS 里面的构造函数,实际上是模拟其他标准的面向对象语言(Java、C#、Python、Ruby、PHP)里面的类(class)
- JS 使用 函数 来模拟其他语言里面的类,引入了
this
和new
var obj = {};
这个其实是和历史有关系
最早 JS 压根儿就没想过用什么类,最早的 JS 的设计就是基于原型对象
创建一个对象也很简单,就是基于原型对象克隆一个,因为早期布兰登艾奇就没想着把 JS 设计得多复杂,早期这门语言对标的是非专业程序员(网页设计师),但是当时(1995 年)全世界范围内都流行 Java、C++ 这种面向对象语言,布兰登艾奇所在的公司(网景)的高层都是 Java 的粉丝,高层就命令布兰登艾奇,在保留 JS 简单性的基础上加入类似于 Java 的特性,让 JavaScript 看上去更像 Java
JavaScript 最早也不叫 JavaScript,而是叫 LiveScript
function Computer(name, price) {
// 1. 创建空对象
// var obj = {}
// 2. 设置原型对象
// obj.__proto__ = Computer.prototype
// 3. 绑定this
// this ----> obj
// 赋值
this.name = name; // {name}
this.price = price; // {name,price}
// 4. 返回this(如果内部没有返回对象的话)
// return this
}
Computer("苹果", 15000); // 当成普通函数调用
new Computer("苹果", 15000); // 通过new当成构造函数调用并生成实例对象
面试题:new 背后做了啥?
- 创建空对象
- 设置原型对象
- 绑定 this
- 返回 this(如果内部没有返回对象的话)
相关信息
声明式语言:HTML、CSS、SQL
命令式语言:C、C++、C#、PHP、Python、Java、JavaScript
2.数据的表达
- JS 本质是在 处理数据
- 提供了三种方式来表达一个数据
- 变量
- 字面量
- 表达式
- 程序中任何需要数据的地方,都可以使用上面任意一种数据表达
1)标识符
- 程序中可以自行命名的地方,称为标识符
- 常见的标识符:
- 变量名
- 函数名
- 参数名
- 必须符合以下规则:
- 允许数字、字母、下划线、$符号
- 不得以数字开头
- 不能和关键字冲突
- 建议使用驼峰命名法
- 一个完整的程序中,会涉及成百上千的标识符,好的名称不仅可以减少名称冲突,更有利于程序的阅读和维护。
- 名称要做到 望文知意
2)转义符
转义符 | 含义 |
---|---|
\' | 普通英文单引号 |
\" | 普通英文双引号 |
\r | 回车 |
\n | 换行 |
小技巧
常用 \r\n
表示换行
3)数据类型
- 原始类型
- number
- string
- boolean
- null
- undefined
- 引用类型
- 对象(包含普通对象、数组、函数)
4)对象的原始写法
- 对象的所有属性名都是 字符串
- 使用单引号
''
或 双引号""
包裹起来 - 当对象的属性名是 纯数字 或 符合标识符规范 时,可以省略引号
- 自动转换为字符串
var obj = {
name: "邓哥",
age: 35,
"graduate date": "2007-7-1",
"home address": {
province: "黑龙江",
city: "city",
},
};
小贴士
书写代码时无须关注这些规则,直接按照简写方式书写属性
若编辑器出现报错,则使用 ''
、""
包裹属性名即可
- 读取对象属性时,使用
[]
,把要读取的属性名传递到中括号中
obj["name"]; // 读取obj的name属性
obj["home address"]; // 读取obj的home address属性
obj["home address"]["province"]; // 读取obj的home address属性(对象)的province属性
- 若属性符合标识符规范,可以使用
.
连接属性名
obj.name; // 读取obj的name属性
obj.age; // 读取obj的age属性
obj["home address"].province; // 读取obj的home address属性(对象)的province属性
5)数组
- 用于表达多个同种类的数据
- 本质就是一个对象
// 数组的对象结构
{
'0': xxx,
'1': xxx,
'2': xxx,
'length': 3
}
3.数据的运算
1)运算符
- 算术(数学)运算
- 支持:加(+)、减(-)、乘(*)、除(/)、求余(%)
+
和-
可以放到单个数据的前面,表示正负- 算术运算的表达式一定返回数字,可以利用其特点做类型转换
- 字符串拼接
- 当
+
的两端有一个是字符串时,不再进行算术运算,而变为字符串拼接 - 表达式一定返回 string,可以利用其特点做类型转换
- 当
- 赋值运算
- 涉及的运算符:
=
+=
*=
/=
-=
%=
。 a += xxx
等效于a = a + (xxx)
,其他类似
- 涉及的运算符:
小贴士
赋值表达式始终返回赋值结果,可以利用该特点完成连续赋值
// 将 3 同时赋值给 a、b
a = b = 3;
计算表达式的阶乘可以使用 **
console.log(2 ** 2); // 4
console.log(2 ** 3); // 8
- 比较运算
- 涉及的运算符:
==
===
!=
!==
>
>=
<
<=
- 涉及的运算符:
小贴士
在实际开发中,没有任何理由使用 ==
和 !=
,可以当做这两个运算符并不存在
应该始终使用 ===
和 !==
来比较相等和不相等
比较运算始终返回 boolean,可以利用这一点来完成某些赋值
// 啰嗦的代码
if (sex === "男") {
user.isMale = true;
} else {
user.isMale = false;
}
// 简洁优雅的代码
user.isMale = sex === "男";
- 逻辑运算
- 逻辑运算会涉及到布尔判定
- 运算符:
!
- 对后面的数据取反,表达式一定返回 boolean
- 可以利用其特点做类型转换
- 运算符:
&&
- 并且,真真为真,其他为假,具有短路规则
- 表达式返回 最后一个判定的数据
- 运算符:
||
- 或者,假假为假,其他为真,具有短路规则
- 表达式返回 最后一个判定的数据
- 运算符:
? :
- 格式
a ? b : c
- 三目运算,判定 a,为真时表达式返回 b,否则返回 c
- 格式
小贴士
在实际的开发中,可以利用短路规则简化代码
// 实现功能,如果exp有值(判定为真),就输出ok
// 啰嗦的代码
if (exp) {
console.log(exp);
}
// 简洁的代码
exp && console.log(exp);
// 实现功能,如果exp有值,就把它的值赋值给n,如果没有值,就给n赋值为默认值 1
// 啰嗦的代码
if (exp) {
n = exp;
} else {
n = 1;
}
// 简洁的代码
n = exp || 1;
三目运算通常用于替代一些简单的 if 结构
// 如果exp为真,则把1赋值给n,否则,把2赋值给n
// 啰嗦的代码
if (exp) {
n = 1;
} else {
n = 2;
}
// 更简洁的代码
n = exp ? 1 : 2;
2)布尔判定
所有需要判断真假的地方都会使用下面的规则
数据 | 判定 |
---|---|
false | false |
null | false |
undefined | false |
0 | false |
NaN | false |
'' | false |
剩余所有数据 | true |
3)类型的隐式转换
- 每个运算符都有自己期望的数据
- 比如
*
期望两端都是数字
- 比如
- 一旦数据不符合运算符的期望,js 就会悄悄的对数据进行类型转换,把它转换成期望的值后进行运算
- 这种转换是 临时 的,并不会对原数据造成影响
小贴士
在实际的开发中,可以利用类型的隐式转换完成以下功能:
var n = +a; // 不管a是啥,都会被转换成数字,保存到n中
var s = a + ""; // 不管a是啥,都会被转换成字符串,保存到s中
var b = !!a; // 不管a是啥,都会被转换成boolean,保存到b中
4.数据的流程
1)for...in
- 遍历键值对的键
for (var key in obj) {
console.log(key + ": " + obj[key]);
}
2)for...of
- 遍历键值对的值
for (var item of list) {
console.log(item);
}
5.流程的切割
1)函数的作用
- 使用函数切割流程
- 可以减少重复代码
- 可以有效的降低整体复杂度
2)如何理解函数的参数、返回值、函数体?
- 参数:表示完成流程所需的 必要信息
- 返回值:表示完成流程后 产生的结果
- 函数体:表示具体的流程
警告
函数的参数、返回值只取决于 函数的作用 ,与函数体无关
3)为什么我觉得有了函数之后,程序反而变得更复杂了?
- 函数的核心作用,是为了让某一段复杂的流程变得简单
- 如果在函数的帮助下,反而觉得流程变得复杂了,极有可能的原因是开发思想没有做相应的切割,导致思想负担过重
- 函数具有三要素
- 函数名
- 参数
- 返回值
- 只要具备三要素,就能书写函数体、完成函数调用
警告
始终记住以下两点:
定义函数时,只需要考虑这个函数如何实现即可,完全不需要考虑其他无关的东西
调用函数时,只需要考虑向其传递什么参数,如何使用它的返回结果,完全无需考虑函数的具体实现
4)学习函数时不知道该如何切割流程怎么办?
- 要完成一个函数声明,分为两步:
- 设计函数
- 设计函数就是如何切割流程,具体来说就是设计出函数的三要素
- 书写函数体
- 根据设计的三要素完成函数体
- 设计函数
(三)核心概念
1.数据的存储和传递
- 原始值
- 存储的是值
- 存储在栈内存中
- 引用值
- 存储的是地址
- 地址也是一个值,只不过这个值是一个地址
- 存储在堆内存中
- 存储的是地址
var user = {
name: "Lucy",
address: {
province: "Guangdong",
city: "Shenzhen",
},
hobby: ["看番", "听歌"],
};
- 给形参赋值,对函数外部的实参无任何影响
function swap(a, b) {
var temp = a;
a = b;
b = temp;
}
console.log(swap(1, 2)); // 传入的两个参数不变
function swap(obj, keys) {
var newObj = {};
for (var key in obj) {
if (keys.includes(key)) {
newObj[key] = obj[key];
}
}
obj = newObj; // 原 obj 实参不变【地址传递】
obj[key] = "a"; // 原 obj 实参改变
}
console.log(
swap(
{
a: 1,
b: 2,
c: 3,
},
["a", "b"],
),
); // 传入的 obj 不变
相关信息
- 平常说的引用传递,其实都是地址传递
- JavaScript 和 Java 其实都没有真正的引用传递,都是地址传递
- C#需要在参数加前缀
ref
声明才能实现引用传递
- C#需要在参数加前缀
- 真正的引用传递,作为参数传进函数后,对该参数的修改会影响到函数外传进来的实参
- 地址传递和值传递,作为参数传进函数后,对参数的修改不会影响到实参
- 引用类型作为参数传进函数后,传的是实参的地址
- 相当于拷贝了实参的地址到新的内存空间,形参存储于该内存空间
var obj = { n: 0 };
function m(obj) {
obj = { n: 1 };
console.log(obj.n);
}
m(obj); // 1
console.log(obj.n); // 0
2.数据的作用域
1)作用域
- 作用域其实就是变量起作用的区域
- 但是 JS 的作用域有自己的特点
- 因为 JS 是词法作用域(静态作用域)
- 需要到创建这个函数的域中取值
- 强调的是 创建,不是调用,这其实就是静态作用域
- JS 有两种作用域
- 全局作用域
- 函数作用域
- 自 ES6 之后引入局部作用域
2)特点
- 内部的作用域能访问外部,反之不行
- 访问时 从内向外 依次查找
- 如果在内部的作用域中访问了外部,则会产生闭包
- 内部作用域能访问的外部,取决于函数定义的位置,和调用的位置无关
- 作用域内定义的变量、函数声明会提升到 作用域 顶部
3)作用域链
- 执行上下文五块转中的一块
- 特点:
- 会向上查找
- 作用域是静态的(词法作用域)
- 在 声明时 就确定下来
var stuname = "zhangsan";
function test() {
console.log(stuname);
}
function test2() {
var stuname = "lisi";
test();
}
test2(); // zhangsan
执行上下文五块转
- 变量环境
- 词法环境
- 作用域链
- this 指向
- 可执行代码
4)闭包
- 闭包是由作用域产生的一种现象
- JavaScript 中所有的函数都是闭包
- JS 中利用静态作用的特性实现的一种功能,称为闭包
- 经常利用闭包的特性来实现一些高级的技巧操作
- 如:函数的柯里化
- 可以把外部的参数固定(缓存参数)
- 如:表单验证输入内容是否正确,验证后执行相应回调
- 如:惰性求值(懒加载数据)
- 如:函数的柯里化
- 当一个函数内部定义了另外一个函数,并且内部函数使用了外部函数的变量时,凸显了闭包作用
- 此时,内部函数可以访问外部函数的变量,因为它们共享了同一个作用域链
- 当外部函数返回内部函数时,内部函数仍然可以访问外部函数的变量
// 函数的柯里化
function outerFunction(x) {
function innerFunction(y) {
return x + y;
}
return innerFunction;
}
const closure = outerFunction(5);
console.log(closure(10)); // 15
// 表单验证,正确xxx,错误xxx
function validateInput(validateFn, successFn, errorFn) {
return function (input) {
if (validateFn(input)) {
successFn(input);
} else {
errorFn(input);
}
};
}
function isEmail(input) {
return /\S+@\S+\.\S+/.test(input);
}
function handleSuccess(input) {
console.log(`验证成功!输入的${input}是正确的email格式`);
}
function handleError(input) {
console.log(`验证失败!输入的${input}是错误的email格式`);
}
const validateEmail = validateInput(isEmail, handleSuccess, handleError);
validateEmail("xxx@163.com");
validateEmail("xxx");
/**
* 惰性求值
* 首先有计算条件,条件达标时进行大消耗的计算
* 如果已经计算的有结果,则下一次不再进行计算
* Vue中的computed即使用该特性
*/
function lazyEvaluation(condition, expensiveOperation) {
let result;
return function (n) {
if (condition(n)) {
if (!result) {
result = expensiveOperation(n);
}
return result;
}
console.log(`参数n的值${n}不符合条件`);
return undefined;
};
}
// 定义判断条件的函数
function isBigNumber(n) {
return n > 100;
}
// 定义大消耗计算的函数
function expensiveOperation(n) {
console.log("===进行了大消耗的计算1===");
console.log("===进行了大消耗的计算2===");
console.log("===进行了大消耗的计算3===");
return n * 10;
}
const lazyFn = lazyEvaluation(isBigNumber, expensiveOperation);
console.log(lazyFn(50)); // undefined
console.log(lazyFn(101)); // ===进行了大消耗的计算1=== ===进行了大消耗的计算2=== ===进行了大消耗的计算3=== 1010
console.log(lazyFn(200)); // 不再重新计算,直接返回上一次缓存的结果 1010
console.log(lazyFn(50)); // undefined
console.log(lazyFn(300)); // 不再重新计算,直接返回上一次缓存的结果 1010
3.全局对象
1)无论是浏览器环境,还是 node 环境,都会提供一个全局对象
- 浏览器环境:window
- node 环境:global
- ES6 提供了自动识别环境的全局对象:globalThis
2)特点
- 全局对象的属性可以被直接访问
- 给未声明的变量赋值,实际就是给全局对象的属性赋值
注意
历史遗留问题
永远别这么干
- 所有的全局变量、全局函数都会附加到全局对象
相关信息
- 这称之为全局污染,又称之为全局暴露,或简称污染、暴露
- 如果要避免污染,需要使用 立即执行函数 改变其作用域
- 立即执行函数又称之为
IIFE
,它的全称是 Immediately Invoked Function Expression - IIFE 通常用于强行改变作用域
// 1.js
var one = (function () {
var a = 1; // 避免污染
var b = 2; // 避免污染
// 暴露为:sayHi
function hello() {
console.log("hello world");
}
// 暴露为:count
var count = 1;
return {
sayHi: hello,
count,
};
})();
// 2.js
(function () {
var a = 3; // 避免污染
var b = 4; // 避免污染
})();
// 使用 1.js 暴露的函数和变量
console.log(one.count);
one.sayHi();
4.构造函数
重要
JS 所有的对象,都是通过构造函数产生的
1)对象
// 语法糖
var obj1 = {
a: 1,
b: 2,
};
var obj2 = new Object(); // 创建一个空对象
obj2.a = 1;
obj2.b = 2;
console.log(obj1, obj2);
2)数组
// 语法糖
var arr1 = [1, 2, 3];
var arr2 = new Array(1, 2, 3); // 创建一个数组
console.log(arr1, arr2);
3)函数
// 语法糖
function sum1(a, b) {
return a + b;
}
var sum2 = new Function("a", "b", "return a+b"); // 创建一个函数
console.log(sum1(1, 2), sum2(1, 2));
5.原型
1)原型要解决的问题
- 上图中,通过构造函数可以创建一个用户对象
- 这种做法有一个严重的缺陷
- 每个用户对象中都拥有一个
sayHi
方法 - 对于每个用户而言,
sayHi
方法是完全一样的 - 没必要为每个用户单独生成一个
- 每个用户对象中都拥有一个
- 要解决这个问题,必须使用原型
2)原型是如何解决的
- 原型
- 每个函数都会自动附带一个属性
prototype
- 这个属性的值是一个普通对象,称之为原型对象
- 每个函数都会自动附带一个属性
- 实例
- instance,通过
new
产生的对象称之为实例 - JS 中所有对象都是通过
new
产生的- 严格来说,JS 中所有对象都称之为实例
- instance,通过
- 隐式原型
- 每个实例都拥有一个特殊的属性
__proto__
- 称之为隐式原型,它指向构造函数的原型
- 每个实例都拥有一个特殊的属性
3)这一切有何意义
- 当访问实例成员时,先找自身,如果不存在,会自动从隐式原型中寻找
- 这样一来,我们可以把那些公共成员,放到函数的原型中,即可被所有实例共享
6.this
- 不同的场景,这 指代的含义不同,JS 中的 this 关键字也是如此
- 也是执行上下文五块砖中的一块
- 在全局代码中使用
this
,指代全局对象- 在真实的开发中,很少在全局代码使用 this
- 在函数中使用 this,它的指向完全取决于函数是如何被调用的
- 和作用域链相反,作用域是在 调用时 确定下来
- 箭头函数中的 this,始终指向全局
- 特殊情况:如果箭头函数被一个普通函数所包裹,箭头函数中的 this 和外面包裹的普通函数 this 一致
- 由外层作用域确定
调用方式 | 示例 | 函数中的 this 指向 |
---|---|---|
通过 new 调用 | new method() | 新对象 |
直接调用 | method() | 全局对象 |
通过对象调用 | obj.method() | 前面的对象 |
call | method.call(ctx) | call 的第一个参数 |
apply | method.apply(ctx) | apply 的第一个参数 |
// 假设在浏览器环境
var name = "HTML";
var obj = {
name: "JS",
showName() {
console.log(this.name);
},
};
var fn = obj.showName;
fn(); // HTML
1)hasOwnProperty
- 判断属性是不是属于对象本身,而不是在隐式原型上
Object.prototype.c = 3;
var obj = {
a: 1,
b: 2,
};
console.log(obj.hasOwnProperty("a")); // true
console.log(obj.hasOwnProperty("c")); // false
2)属性名 in 对象
- 判断属性名是否在对象自身及其隐式原型上
console.log("c" in obj); // true
7.原型链
1)什么是原型链
重要
所有的对象都是通过 new 函数
的方式创建的
console.log(typeof Object); // "function",因为 Object() 是构造函数
// 对象 u1 通过 new User 创建
var u1 = new User("邓", "旭明");
// 对象 u2 通过 new Object 创建
var u2 = {
firstName: "莫",
lastName: "妮卡",
};
// 等效于
var u2 = new Object();
u2.firstName = "莫";
u2.lastName = "妮卡";
上面的代码形成的原型图如下
原型对象本身也是一个对象,默认情况下,是通过 new Object
创建的,因此,上面的两幅原型图是可以发生关联的
注意
Object.prototype.__proto__
比较特殊
固定指向 null
u1 的隐式原型形成了一个链条,称之为 原型链
当读取对象成员时,会先看对象自身是否有该成员,如果没有,就依次在其原型链上查找
2)完整的链条
Function
是底层源码用 C++/Java 实现的,其隐式原型和原型都指向Function原型对象
Object原型对象
的属性和方法能影响到所有原型对象
// 判断是不是函数
Function.prototype.isFunc = true;
function sum() {}
console.log(sum.isFunc); // true
var obj = {};
console.log(obj.isFunc); // undefined
3)对开发的影响
- 更改构造函数的原型会对 所有原型链上有该构造函数的原型的对象 产生影响
- 利用原型链判断类型
instanceof
关键字【常用】a instanceof b
可以读作a 是不是 b
Object.getPrototypeOf()
【不常用】obj.__proto__ === Object.getPrototypeOf(obj)
- 创建空原型的对象
Object.create(null)
Object.setPrototypeOf(obj, null)
// 判断object的原型链中,是否存在constructor的原型
object instanceof constructor;
// 返回object的隐式原型
Object.getPrototypeOf(object);
// 返回一个新对象,新对象以target作为隐式原型
Object.create(target);
// 设置obj的隐式原型为prototype
Object.setPrototypeOf(obj, prototype);
4)几句口诀
- 原型的本质其实就是一个对象
- 所有的函数都有原型属性 prototype
- prototype 中默认包含一个属性:constructor,该属性指向函数对象本身
- 所有的对象都有隐式对象
__proto__
- 隐式原型
__proto__
是一个对象,指向该对象的构造函数的原型 prototype - 在查找对象成员时,若对象本身没有该对象,则会到隐式原型中查找
- 所有的函数的隐式原型,都指向 Function 函数的原型 prototype
- 所有函数原型的隐式原型,都指向 Object 原型
- 有两个特殊情况
- Function 的隐式原型指向自己的原型对象
- Object 原型的隐式原型指向 null
5)练习
function User() {}
User.prototype.sayHello = function () {};
const u1 = new User();
const u2 = new User();
console.log(u1.sayHello === u2.sayHello); // true
console.log(User.prototype.constructor); // function User() {}
console.log(u1.__proto__ === u2.__proto__); // true
console.log(u1.__proto__ === User.__proto__); // false
console.log(User.__proto__ === Function.prototype); // true
console.log(User.__proto__ === Function.__proto__); // true
console.log(Object.__proto__ === Function.__proto__); // true
console.log(Object.prototype.__proto__ === Function.prototype.__proto__); // false
console.log(Object.prototype === Function.prototype.__proto__); // true
var F = function () {};
Object.prototype.a = function a() {};
Function.prototype.b = function b() {};
var f = new F();
console.log(f.a, f.b, F.a, F.b); // 提示:顺着线查找
// function a(){} undefined function a(){} function b() {}
8.继承
1)会员系统
- 视频网站有两种会员:
- 普通会员
- 属性:用户名、密码
- 方法:观看免费视频
- VIP 会员
- 属性:普通会员的所有属性、会员到期时间
- 方法:普通会员的所有方法、观看付费视频
- 普通会员
// 普通会员的构造函数
function User(username, password) {
this.username = username;
this.password = password;
}
User.prototype.playFreeVideo = function () {
console.log("观看免费视频");
};
// VIP会员的构造函数
function VIPUser(username, password, expires) {
this.username = username;
this.password = password;
this.expires = expires;
}
VIPUser.prototype.playFreeVideo = function () {
console.log("观看免费视频");
};
VIPUser.prototype.playPayVideo = function () {
console.log("观看付费视频");
};
- 上面的代码出现了两处重复代码:
- VIPUser 的构造函数中包含重复代码
- VIPUser 的原型上包含了重复代码
// 这段代码和User构造函数并没有区别,将来也不会有区别,即:普通用户该有的属性,VIP用户一定有
this.username = username;
this.password = password;
// 这个方法和User上的同名方法逻辑完全一致,将来也不会有区别,即:普通用户该有的方法,VIP用户一定有
VIPUser.prototype.playFreeVideo = function () {
console.log("观看免费视频");
};
2)处理构造器内部的重复
function VIPUser(username, password, expires) {
User.call(this, username, password);
this.expires = expires;
}
3)处理原型上的重复
Object.setPrototypeOf(VIPUser.prototype, User.prototype);
4)这和继承有什么关系
- 继承是面向对象的概念,它描述了两个对象类型(类,构造函数)之间的关系
- 如果在逻辑上可以描述为:
- A 不一定是 B,但 B 一定是 A
- 则:B 继承 A、A 派生 B、A 是 B 的父类、B 是 A 的子类
重要
子类的实例应该自动拥有父类的所有成员
- 继承具有两个特性:
- 单根性:子类最多只有一个父类
- 传递性:间接父类的成员会传递到子类中
5)如何在 JS 中封装继承
function inherit(Child, Parent) {
// 在原型链上完成继承
Object.setPrototypeOf(Child.prototype, Parent.prototype);
// 相当于:Child.prototype.__proto__ = Parent.prototype
}
过去,由于没有提供更改隐式原型的方法,因此这一过程会比较复杂
那时候,我们使用一种称之为「圣杯模式」的办法来达到相同的目的
9.浏览器的渲染流程
1)解析 HTML
- 浏览器从网络(
http[s]://
)或本地文件(file://
)中获取到 HTML 源代码,然后从上到下的解析源代码 - 若解析过程中,读取到 CSS 或 JS, 停止解析(阻塞) ,转而解析 CSS 或执行 JS
<!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>Document</title>
<link rel="stylesheet" href="./css/index.css" />
</head>
<body>
<h1>Hello World!</h1>
<script src="./js/index.js"></script>
<p>Lorem</p>
</body>
</html>
为什么要将 CSS 写到页面的开头,而 JS 写到页面的最后?
将 CSS 写到页面开头,是为了让浏览器尽快读取并解析样式,避免给用户看到丑陋的页面,也是为了避免页面闪烁
将 JS 代码写到最后,是为了让浏览器尽快呈现页面给用户,然后再执行 JS 完成交互功能
2)生成 DOM 树
重要
浏览器会一边解析 HTML,一边生成 DOM 树
- 在 JS 中获取到的 DOM 就是 DOM 树中的 DOM
- 当 DOM 树完全生成好后,会触发
DOMContentLoaded
事件 - 当页面中的所有外部资源全部加载完毕后,会触发
load
事件load
事件也可以针对单个外部资源使用,资源加载完成后触发
document.addEventListener("DOMContentLoaded", function () {
console.log("DOM树已全部生成完毕");
});
window.onload = function () {
console.log("所有资源已加载完成");
};
3)生成渲染树
重要
浏览器一边生成 DOM 树,一边计算 DOM 树中每个节点的样式规则,最终形成渲染树
- CSS 属性的计算过程,发生在这一个步骤
4)布局 layout / 重排 reflow
- 这个步骤又称之为 reflow(回流、重排)
- 是指浏览器一边生成渲染树,一边计算每个元素最终的尺寸和位置
- 完成后,页面中的所有元素的位置和尺寸就确定下来了,即将被渲染到页面
- 这个步骤会在页面之后的运行过程中不断的重复
以下 JS 操作均会导致 reflow:
- 获取元素的尺寸和位置
- 直接或间接改变元素的尺寸和位置
- reflow 非常耗时,浏览器为了提升性能,对 JS 中 连续 导致 reflow 的代码
- 会把 reflow 的时间点延迟到结束后进行
- 但在此过程中,如果遇到了获取尺寸和位置的代码,浏览器会迫不得已立即 reflow
dom.style.width = "100px";
dom.style.height = "200px";
dom.style.left = "10px";
dom.style.top = "10px";
dom.style.width = "100px";
dom.style.height = "200px";
dom.clientHeight; // 读取高度,导致强行reflow
dom.style.left = "10px";
dom.style.top = "10px";
- 如果没有获取尺寸和位置的代码,仅仅是触发重绘的代码,也不会立即 reflow
// 下面代码只会导致 1次 reflow
dom.style.width = "100px";
dom.style.height = "200px";
dom.style.color = "#f40";
dom.style.left = "10px";
5)重绘 repaint
- 浏览器一边 reflow,一边进行生成对应的图形绘制到页面,绘制的过程称之为 repaint
- 绘制的过程是靠 GPU 完成的,速度非常快
- 相对于导致 reflow 的代码,仅会导致 repaint 的代码效率会高出很多
重要
1.所有会导致 reflow 的代码,均会导致 repaint
2.凡是不会影响盒子排列,仅影响盒子外观的代码都不会导致 reflow,仅导致 repaint
- 改变背景颜色
- 改变字体颜色
- 圆角边框
- 背景图
- ......
10.异常
- 异常并非坏事,它可以让开发人员及时发现错误、定位错误
- 甚至在某些时候还需要故意的抛出异常
1)异常的分类
- 在 JS 中,异常表现为一个对象
- 不同的对象表达了不同的异常类型
- 不同类型的异常对应到不同的错误
- 每个异常都是一个对象,通过对应的构造函数创建
- 所有的异常构造器都继承自 Error,更多信息参见 MDN 官方文档
异常类型 | 含义 |
---|---|
SyntaxError | 语法错误 |
ReferenceError | 引用错误,往往是使用了未定义的变量或函数 |
TypeError | 类型错误,往往是使用了一个对象中不存在的成员 |
- 当代码运行过程中出现错误时,JS 会
- 自动创建对应的异常对象,抛出错误
- 程序终止运行
- 控制台中会显示异常对象
- 每个异常对象都至少记录了 两个关键信息
- 错误消息描述:描述异常出现的原因
- 调用堆栈信息:描述异常出现的位置
2)捕获异常
- 捕获异常就是处理错误
- 当错误发生后,我们对错误进行相应的处理,让程序不至于终止
try {
// 代码1
} catch (err) {
// 代码2:当代码1出现异常后,会执行这里的代码,异常对象会传递给err
} finally {
// 代码3:可省略。无论是否有异常,都会执行
}
// 无异常的执行顺序:代码1 --> 代码3
// 有异常的执行顺序:代码1 --> 出现异常,中断代码1的执行 --> 代码2 --> 代码3
- 在绝大部分时候,我们都无须捕获异常,除非满足以下要求:
- 我们能够预知某段代码会出现异常
- 我们知道出现异常后要做什么
- 以上条件任意一个不满足,都不应该处理异常
注意
永远不能为了不报错而捕获异常!
// 下面是一段可能使用异常捕获的伪代码
try {
var heros = network.getHeros(); // 从网络获取王者荣耀英雄数据,得到英雄数组
createHTML(heros); // 将数组生成HTML
} catch (err) {
// 出现网络故障,给用户显示一个提示框
showErrorDialog("网络故障,请检查您的网络是否连接正常。故障原因:" + err.message);
}
3)手动抛出异常
- 不仅浏览器会自动给我们抛出异常,还可以手动的抛出异常
// 当代码运行到这里,会终止执行,抛出异常对象,效果和浏览器抛出的错误完全一样
throw 异常对象;
- 当编写函数时,如果满足下面三个条件,就可以选择抛出异常:
- 预知执行过程中可能会出现某种错误
- 浏览器不会抛出这个错误
- 该函数无法处理这个错误
// 下面展现了一个需要抛出异常的例子
/**
* 得到两个数字之和
* 若传递的不是数字,则会抛出TypeError
* @param {number} a 数字1
* @param {number} b 数字2
* @return {number} 两数之和
*/
function sum(a, b) {
if (typeof a !== "number" || typeof b !== "number") {
throw new TypeError("必须传入两个数字才能求和");
}
return a + b;
}
规范
如果某个函数需要抛出异常,一定要在函数的文档注释中阐述清楚
4)练习
说出下面的错误描述的含义,以及该错误发生的原因
- 含义:不能调用对象的 sayHi 方法
- 原因:该对象值为 null
- 含义:不能访问对象的 name 属性
- 原因:该对象值为 undefined
- 含义:变量 b 未定义
- 原因:在变量 b 声明前访问了变量 b
- 含义:对象 a 的 name 属性不是一个函数
- 原因:使用调用函数的方式访问了对象 a 的 name 属性
11.执行上下文
- JS 属于解释型语言
- JS 执行分为解释和执行两个阶段
1)解释阶段
- 词法分析
- 语法分析
- 作用域规则的确定
2)执行阶段
- 创建执行上下文
- 执行函数代码
- 垃圾回收
3)执行上下文
- 会在创建阶段确定几个问题
- 确定所有形参值以及特殊的变量 arguments
- 函数中所有 var 声明的变量,将值设为 undefined,如果 VO 中已经有该变量则覆盖
- 函数中通过字面量声明的函数,将值设为指向函数的对象
- 最重要的是 Variable Object(VO)
(四)标准库
- 库:Library,API 的集合
- API:Application Program Interface,应用程序编程接口
- 标准:定义 API 的定义和功能
- 具体实现由浏览器或 node 完成
1.静态 & 实例
1)静态方法
- 通过函数名调用
- 如:Number.isNaN()
- Number 是函数
- isNaN 也是函数
function S() {}
S.abc = function () {}; // abc是静态方法
2)实例方法
- 通过实例对象调用
- 绑定在原型对象上
- 如:Number.prototype.toFixed()
function S() {}
S.prototype.def = function () {};
new S().def(); // def是实例方法
3)静态成员
- 通过构造函数调用
function A() {}
A.c = 3; // c是静态成员
4)实例成员
- 通过实例对象调用
- 绑定在原型对象上
function A() {
this.a = 1; // a是实例成员
}
A.prototype.b = 2; // b是实例成员
2.包装类
如果尝试着把原始类型(number、string、boolean)当做对象使用,JS 会自动将其转换为对应包装类的实例
1)Number
API | 含义 | 备注 |
---|---|---|
Number.NaN | 表示一个数学上并不存在的数字 | 可以直接书写为 NaN |
Number.isNaN() | 判断传入的值是否是 NaN | 可以直接书写为 isNaN |
Number.isInteger() | 判断传入的值是否是整数 | |
Number.parseInt() | 把传入的值转换为整数形式返回 | 可以直接书写为 parseInt() |
Number.parseFloat() | 把传入的值转换为小数形式返回 | 可以直接书写为 parseFloat() |
Number.prototype.toFixed() | 将当前数字保留指定位数的小数返回 | 传入小数位数 |
Number.prototype.toString() | 将当前数字转换为字符串返回 | 传入进制 2-36 |
2)String
API | 含义 | 备注 |
---|---|---|
String.fromCharCode() | 根据编码值得到一个字符 | 传入一个或多个编码值 |
String.prototype.length | 得到字符串的长度 | |
String.prototype.charCodeAt() | 得到某个下标的字符编码 | 传入下标 |
String.prototype.includes() | 判断当前字符串是否包含某个子串 | 传入子串 |
String.prototype.indexOf() | 判断某个字符串在当前字符串中的第一个下标位置 | 如果没有,返回-1 |
String.prototype.lastIndexOf() | 判断某个字符串在当前字符串中的最后一个下标位置 | 如果没有,返回-1 |
String.prototype.endsWith() | 判断某个字符串是否以指定的字符串结束 | 传入一个字符串 |
String.prototype.startsWith() | 判断某个字符串是否以指定的字符串开始 | 传入一个字符串 |
String.prototype.padStart() | 将当前的字符串按照指定的字符在字符串开始位置填充到指定的位数,返回填充后的字符串 | 传入位数、填充字符 |
String.prototype.padEnd() | 将当前的字符串按照指定的字符在字符串结束位置填充到指定的位数,返回填充后的字符串 | 传入位数、填充字符 |
String.prototype.split() | 把当前字符串按照某个字符串分割成一个字符串数组返回 | 传入分隔符 |
String.prototype.substring() | 返回一个字符串在开始索引到结束索引之间的一个子集, 或从开始索引直到字符串的末尾的一个子集 | 传入开始字符、结束字符 |
String.prototype.trim() | 从字符串的两端删除空白字符,返回新字符串 | 无参数 |
String.prototype.trimStart() | 从字符串的开头删除空白字符,返回新字符串 | 无参数 |
String.prototype.trimEnd() | 从字符串的末端删除空白字符,返回新字符串 | 无参数 |
String.prototype.toUpperCase() | 将调用该方法的字符串转为大写形式并返回 | 无参数 |
String.prototype.toLowerCase() | 将调用该方法的字符串转为小写形式并返回 | 无参数 |
String.prototype.replace() | 替换字符串中的第一个对应字符为新字符 | |
String.prototype.replaceAll() | 替换字符串中的所有对应字符为新字符 |
3.数学
API | 含义 | 备注 |
---|---|---|
Math.PI | 得到圆周率 π | |
Math.abs() | 求某个数绝对值 | 传入一个数 |
Math.ceil() | 向上取整 | 传入一个数 |
Math.floor() | 向下取整 | 传入一个数 |
Math.max() | 求一个数列中的最大值 | 把数列依次传入 |
Math.min() | 求一个数列中的最小值 | 把数列依次传入 |
Math.random() | 得到一个 0-1 之间的随机小数 | 无参;无法取到 1 |
Math.round() | 返回四舍五入的结果 | 传入一个数 |
相关信息
parseInt() 是向 0 取整
4.日期
1)单位
单位 | 名称 | 换算 |
---|---|---|
hour | 小时 | 1 day = 24 hours |
minute | 分钟 | 1 hour = 60 minutes |
second | 秒 | 1 minute = 60 seconds |
millisecond (ms) | 毫秒 | 1 second = 1000 ms |
nanosecond (ns) | 纳秒 | 1 ms = 1000 ns |
2)GMT 和 UTC
- 世界划分为 24 个时区,北京在东 8 区,格林威治在 0 时区
- 时区
- GMT:Greenwich Mean Time 格林威治世界时
- 太阳时,精确到毫秒
- UTC:Universal Time Coordinated 世界协调时
- 以原子时间为计时标准,精确到纳秒
- 国际标准中,已全面使用 UTC 时间,而不再使用 GMT 时间
- GMT 和 UTC 时间在文本表示格式上是一致的
- 均为
星期缩写, 日期 月份 年份 时间 GMT
- 例如:
Thu, 27 Aug 2020 08:01:44 GMT
- 均为
- ISO 8601 标准规定,建议使用以下方式表示时间:
YYYY-MM-DDTHH:mm:ss.msZ
- 例如:
2020-08-27T08:01:44.000Z
注意
GMT、UTC、ISO 8601 都表示的是 零时区 的时间
3)Unix 时间戳
- Unix 时间戳(Unix Timestamp)是 Unix 系统最早提出的概念
- 将 UTC 时间 1970 年 1 月 1 日凌晨作为起始时间,到指定时间经过的秒数(毫秒数)
4)程序中的时间处理
- 程序对时间的计算、存储务必使用 UTC 时间 或者 时间戳
- 在和用户交互时,将 UTC 时间或时间戳转换为更加友好的文本
- 用户的生日是本地时间还是 UTC 时间?
- 本地时间
- 如果要比较两个日期的大小,是比较本地时间还是比较 UTC 时间?
- UTC 时间
- 如果要显示文章的发布日期,是显示本地时间还是显示 UTC 时间?
- 本地 时间
北京时间2020-8-28 10:00:00
和格林威治2020-8-28 02:00:00
,两个时间哪个大,哪个小?- 一样大
北京的时间戳为0
和格林威治的时间戳为0
,它们的时间一样吗?- 一样,时间戳都是指零时区
- 一个中国用户注册时填写的生日是
1970-1-1
,它出生的 UTC 时间是多少?时间戳是多少?- UTC:
31 Dec 1969 16:00:00 GMT
- 时间戳:
-8 * 60 * 60 * 1000 ns
=>-28800000ns
- UTC:
let date = new Date(1970, 0, 1);
console.log(date.getTime()); // -28800000
5)日期 API
// 构造函数
// 得到一个当前日期对象
new Date();
// 根据时间戳得到一个日期对象
new Date(value);
// 根据一个标准日期字符串得到一个日期对象
new Date(dateString);
// 根据年、月、日、小时、分钟、秒、毫秒得到一个日期对象
new Date(year, monthIndex [, day [, hours [, minutes [, seconds [, milliseconds]]]]]);
API | 含义 | 备注 |
---|---|---|
Date.now() | 得到当前时间戳 | 无参 |
Date.prototype.getFullYear() | 得到年 | 无参;本地时间; |
Date.prototype.getMonth() | 得到月 | 无参;本地时间;范围 0-11 |
Date.prototype.getDate() | 得到日 | 无参;本地时间; |
Date.prototype.getHours() | 得到小时 | 无参;本地时间; |
Date.prototype.getMinutes() | 得到分钟 | 无参;本地时间; |
Date.prototype.getSeconds() | 得到秒 | 无参;本地时间; |
Date.prototype.getMilliseconds() | 得到毫秒 | 无参;本地时间; |
Date.prototype.toLocaleString() | 得到日期本地的表示方式 |
5.对象
API | 含义 | 备注 |
---|---|---|
Object.assign() | 将多个对象的属性混合到一起 | 后面覆盖前面 |
Object.getPrototypeOf() | 获取一个对象的隐式原型 | |
Object.setPrototypeOf() | 设置一个对象的隐式原型 | |
Object.create() | 创建一个新对象,同时设置新对象的隐式原型 |
6.数组
API | 含义 | 备注 |
---|---|---|
Array.prototype.concat() | 把多个数组拼接成一个 | |
Array.prototype.includes() | 判断数组中是否包含某个值 | |
Array.prototype.indexOf() | 得到数组中某个值的第一个下标 | 若不存在则返回-1 |
Array.prototype.lastIndexOf() | 得到数组中某个值的最后一个下标 | 若不存在则返回-1 |
Array.prototype.join() | 把数组中每一项使用某个字符连接起来,形成一个字符串返回 | |
Array.prototype.push() | 向数组的末尾添加一项 | |
Array.prototype.unshift() | 向数组的开头添加一项 | |
Array.prototype.pop() | 删除数组最后一项 | 返回被删除的值 |
Array.prototype.shift() | 删除数组第一项 | 返回被删除的值 |
Array.prototype.splice() | 删除、修改、插入任何位置的值 | |
Array.prototype.reverse() | 将数组中的元素顺序颠倒 | |
Array.prototype.sort() | 对数组进行排序 | 传入比较函数:0-位置不变,<0-前者在前,>0-前者在后 |
Array.prototype.slice() | 对数组进行切割 |
7.函数
- 当函数作为参数传递时,该函数称为回调函数
API | 含义 | 备注 |
---|---|---|
Function.prototype.apply() | 执行函数,绑定 this | 参数列表以数组的形式传递 |
Function.prototype.call() | 执行函数,绑定 this | 参数列表依次传递 |
8.正则表达式
- 判断字符串是否满足某个规则
- 这个规则就是正则表达式
- 脱离编程语言的规范,不局限于 JS
- 通过某种规范定义描述字符串的一些规则
1)创建正则对象
- 标识可缺省
// 构造函数
new RegExp("规则", "标识");
// 字面量书写
/规则/标识
2)正则常用方法
// reg是正则对象
reg.test("字符串"); // 验证字符串是否满足规则
// reg是正则对象,str是字符串
str.replace(reg, "替换目标"); // 将字符串中匹配正则的部分替换为目标
// 将字符串中匹配正则的部分传入到回调函数的参数中,将函数的返回结果进行替换
str.replace(reg, function (s) {
return "替换目标";
});
3)标识
标识字符 | 含义 |
---|---|
i(ignore) | 不区分大小写 |
g(global) | 全局匹配,如果没有此标识,只会匹配第一个 |
m(multiline) | 多行匹配 |
4)规则
- 字符匹配规则
规则书写 | 含义 |
---|---|
直接书写一个普通字符 | 匹配书写的字符 |
[字符规则] | 匹配[]中出现的所有字符规则(匹配一个) |
[^字符串规则] | 匹配[]中 没有 出现的字符规则 |
. | 匹配任意字符 |
\d | 匹配数字,等价于 [0-9] |
\D | 匹配非数字 |
\s | 匹配空白字符,包括空格、回车、换行、制表 |
\S | 匹配所有非空白字符 |
\w | 匹配单词字符,等价于 [A-Za-z0-9_] |
\W | 匹配非单词字符,等价于 [^A-Za-z0-9_] |
^ | 匹配字符串开始,写到规则开始位置 |
$ | 匹配字符串结束,写到规则结束位置 |
\\ | 匹配 \ |
- 连续的规则
- 多个规则可以连续书写,用以匹配多个字符
- 若多个规则是 或者 的关系,使用
|
分割
/\d[a-zA-Z]/ // 匹配以1个数字紧跟一个字母
/\d[a-zA-Z]|[a-zA-Z]\d/ // 匹配以1个数字紧跟一个字母,或者一个字母紧跟一个数字
- 规则的重复(量词)
- 一个或一段规则之后,可以紧跟一个量词
- 表示前面的规则出现的次数
/[a-zA-Z]\d{3}/ // 匹配1个字母,后面跟上连续的3个数字,{3}是量词,应用的规则是\d
/([a-zA-Z]\d){3}/ // {3}是量词,应用的规则是 [a-zA-Z]\d
量词 | 含义 |
---|---|
{n} | 出现 n 次 |
{n, m} | 出现 n-m 次 |
{n,} | 至少出现 n 次 |
* | 出现 0 次或多次,等价于 {0,} |
? | 出现 0 次或一次,等价于 {0,1} |
+ | 出现 1 次或多次,等价于 {1,} |
(五)WebAPI
- 和标准库不同,WebAPI 是 浏览器 提供的一套 API
- 用于操作浏览器窗口和界面
- WebAPI 中包含两个部分:
- BOM:Browser Object Model,浏览器模型
- 提供和浏览器相关的操作
- DOM:Document Object Model,文档模型
- 提供和页面相关的操作
- BOM:Browser Object Model,浏览器模型
- EcmaScript 是官方标准
- 支持 JS 的环境必须支持 ES
- NodeJS 环境支持 EcmaScript+NodeAPI
1.BOM
BOM 提供了一系列的对象和函数,提供和浏览器本身相关的操作
1)window
- 全局对象
- MDN 官方文档
API | 含义 | 备注 |
---|---|---|
open() | 打开一个新的浏览器窗口 | 返回新窗口的 window 对象 |
close() | 关闭浏览器窗口 | 只能关闭使用 open 打开的浏览器窗口 |
setTimeout() | 设置一个计时器 在一段时间后自动执行某个函数 | 参数 1:函数,无参,this 指向 window 参数 2:时间,毫秒 返回:计时器的 ID |
clearTimeout() | 清除指定 ID 的计时器 | 传入计时器的 ID |
setInterval() | 设置一个计时器 每隔一段时间自动执行某个函数 | 参数 1:函数,无参,this 指向 window 参数 2:时间,毫秒 返回:计时器的 ID |
clearInterval() | 清除指定 ID 的计时器 | 传入计时器的 ID |
alert() | 弹出提示框 | 会阻塞,不同的操作系统外观有差异 |
confirm() | 弹出确认框 | 会阻塞,不同的操作系统外观有差异 |
2)window.location
- 提供地址栏的相关操作
- MDN 官方文档
API | 含义 | 备注 |
---|---|---|
location.href | 获取或设置页面当前地址 | 设置地址会导致页面跳转 |
location.protocol | 获取或设置地址中的协议部分 | |
location.host | 获取或设置地址中的主机名和端口号 | |
location.hostname | 获取或设置地址中的主机名 | |
location.port | 获取或设置地址中的端口号 | |
location.pathname | 获取或设置地址中的路径部分 | |
location.search | 获取或设置地址中的参数部分 | |
location.hash | 获取或设置地址中的 hash 部分 | |
location.reload() | 刷新页面 |
3)window.history
- 提供 当前窗口(不是浏览器) 历史记录的操作
- MDN 官方文档
API | 含义 | 备注 |
---|---|---|
history.back() | 后退 | |
history.forward() | 前进 | |
history.go() | 根据相对当前页面的偏移量, 进入指定的记录页 | |
history.pushState() | 在历史记录中添加一条记录 | 页面不刷新 |
history.replaceState() | 替换当前记录 | 页面不刷新 |
2.DOM
DOM 是一个对象,它对应到 HTML 中的节点
3.获取 dom
- 获取的内容是 HTMLCollections,看着好像是数组,但是其实不是,是一个伪数组
document.querySelectorAll
- 这个函数获取的才是一个可以直接使用数组函数的 NodeList
- 控制台可以使用
$()
表示 document.querySelector()- 使用
$$()
表示 document.querySelectorAll() - 当前网页没有引入 JQuery 才可以
- 使用
API | 含义 | 备注 |
---|---|---|
document.getElementById() | 根据元素 id 获取 dom | 得到单个 dom |
document.getElementsByTagName() dom.getElementsByTagName() | 根据元素名称获取 dom | 得到 dom 的伪数组 |
document.getElementsByClassName() dom.getElementsByClassName() | 根据元素类样式获取 dom | 得到 dom 的伪数组 |
document.querySelector() dom.querySelector() | 根据 CSS 选择器获取 dom | 得到第一个匹配的 dom |
document.querySelectorAll() dom.querySelectorAll() | 根据 CSS 选择器获取 dom | 得到所有匹配的 dom 静态(非实时)伪数组 |
document.documentElement | 获取 html 元素 | |
document.body | 获取 body | |
document.head | 获取 head | |
dom.children | 获取 dom 的子元素 | 得到 dom 的伪数组 |
dom.childNodes | 获取 dom 的子节点 | 得到 dom 节点的伪数组 关于节点对象 |
dom.previousElementSibling | 得到 dom 前一个兄弟元素 | |
dom.nextElementSibling | 得到 dom 后一个兄弟元素 | |
dom.parentElement | 得到 dom 的父元素 |
4.创建 dom
API | 含义 | 备注 |
---|---|---|
document.createElement() | 创建一个 dom 并返回 | 传入元素名称 |
5.更改 dom 结构
这里是指更改文档树(DOM 树)
API | 含义 | 备注 |
---|---|---|
dom.remove() | 从文档树中删除 dom | 不是删除对象 |
dom.removeChild() | 删除 dom 的某个子节点 | 传入 dom 对象 |
dom.insertBefore() | 在 dom 的子节点中,添加一个新节点到另一个节点之前 | |
dom.appendChild() | 添加一个新节点到 dom 的子节点末尾 | 传入 dom 对象 |
6.dom 属性
属性是指 HTML 元素的属性
1)标准属性
- HTML 元素本身拥有的属性,例如:
- a 元素的 href、title
- input 的 value
- img 的 src
- ......
- 所有标准属性均可通过
dom.属性名
得到- 布尔属性会被自动转换为 boolean
- 路径类的属性会被转换为 绝对路径
- 标准属性始终都是存在的,都有默认值,不管是否有在元素中设置该属性
- class 由于和关键字重名,因此需要使用 className
2)自定义属性
- HTML 元素标准中未定义的属性
- 所有的自定义属性均可通过下面的方式操作:
- 设置属性键值对:
dom.setAttribute(name, value)
- 获取属性值:
dom.getAttribute(name)
- 设置属性键值对:
注意
- 自定义属性和元素源码书写是对应的
- 路径类不会转换为绝对路径
<a href="1.js"></a>
var a = document.querySelector(a);
a.href; // "http://loclahost:5500/1.js"
a.getAttribute("href"); // "1.js"
a.setAttribute("test", "test");
a.getAttribute("test"); // "test"
7.dom 内容
API | 含义 | 备注 |
---|---|---|
dom.innerText | 获取或设置元素文本内容 | 设置时会自动进行 HTML 实体编码 |
dom.innerHTML | 获取或设置元素的 HTML 内容 |
8.dom 样式
1)内联样式
- 元素的 style 属性中书写的样式
2)计算样式(最终样式)
- 元素最终计算出来的样式
警告
JS 可以获取内联样式和计算样式,但只能设置内联样式
3)样式的常见操作
dom.style
- 获取元素的内联样式,得到样式对象
- 对象中的所有样式属性均可以被赋值,赋值后即可应用样式到元素的 style 中
getComputedStyle(dom)
- 获取元素的计算样式,得到一个样式对象
- 该样式对象中的属性是只读的,无法被重新赋值
关于样式对象
- 当给样式赋值为空字符串时,相当于删除内联样式
- 当给样式的赋值不合法时,赋值语句无效,不会报错
- CSS 的短横线命名法,在属性名中表现为驼峰命名法
9.监听 dom 事件
- 监听事件可以描述为:
- 某个 DOM 发生了 某件事 之后,需要做 某些处理
- 某个 DOM:监听谁?
- 某件事(事件类型):发生了什么?
- 某些处理(处理函数):需要做什么?
// 为dom注册点击事件,当被点击时,自动运行事件处理函数
dom.onclick = function () {
console.log("dom 被点击了");
};
1)事件类型
- 表单类事件
事件名称 | 触发时机 | 备注 |
---|---|---|
submit | 表单被提交时触发 | 注册到 form 元素上 |
input | 文本框改变后立即触发 | 注册到 input、textarea 上 |
change | 文本框改变后、失去焦点时触发 下拉列表、多选框、单选框改变后立即触发 | 注册到 input、select、textarea 上 |
reset | 表单被重置时触发 | 注册到 form 元素上 |
focus | 元素聚焦时触发 | |
blur | 元素失去焦点时触发 |
- 鼠标类事件
事件名称 | 触发时机 | 备注 |
---|---|---|
click | 鼠标按下抬起后触发 | |
contextmenu | 右键菜单显示前触发 | |
mousedown | 鼠标按下时触发 | |
mouseup | 鼠标抬起时触发 | |
mousemove | 鼠标在元素上移动时触发 | |
mouseenter | 鼠标进入元素时触发(不冒泡) | |
mouseleave | 鼠标离开元素时触发(不冒泡) | |
mouseover | 鼠标进入元素时触发(冒泡) | |
mouseout | 鼠标离开元素时触发(冒泡) | |
wheel | 鼠标滚轮滚动时触发 |
- 键盘事件
事件名称 | 触发时机 | 备注 |
---|---|---|
keydown | 某个键被按下时触发 | |
keyup | 某个键被抬起时触发 |
2)注册事件
- 方式 1:将事件注册写到元素上【基本被弃用】
<button onclick="js代码">按钮</button>
- 方式 2:使用 dom 属性注册事件
- 属性名为
on + 事件类型
- 优点
- 易于监听、覆盖、移除
- 缺点
- 只能注册一个处理函数
- 某些事件不支持用这种方式注册
- 属性名为
// 监听事件
dom.onclick = function () {
// 处理函数
};
// 移除监听事件
dom.onclick = null;
- 方式 3:使用 addEventListener 方法注册事件【最完美的事件注册方式】
dom.addEventListener("click", function () {
// 处理函数1
});
dom.addEventListener("click", function () {
// 处理函数2
});
// 如果要移除用这种方式注册的事件,需要改写代码
function handler1() {
// 处理函数1
}
function handler2() {
// 处理函数2
}
dom.addEventListener("click", handler1);
dom.addEventListener("click", handler2);
dom.removeEventListener("click", handler1); // 移除监听函数1
3)事件处理函数
- 当事件发生时,会自动调用事件处理函数,并向函数传递一个参数
- 该参数称之为事件对象,里面包含了事件发生的相关信息
- 比如鼠标位置、键盘按键等等
- 在事件处理函数中,
this
始终指向注册事件的 dom- 即使绑定事件后,dom 指向的对象修改了,也不影响 this
dom.addEventListener("click", function (e) {
console.log(e.clientX); //打印鼠标的横坐标
console.log(this); // dom
});
dom = null; // this还是指向dom原来的对象
10.dom 进阶
1)事件默认行为
- 对于某些元素的某些事件,浏览器会有自己的默认行为,比如:
- a 元素的 click 事件,浏览器会跳转页面
- form 元素的 submit 事件,浏览器会提交表单,最终导致页面刷新
- 文本框的 keydown 事件,浏览器会将按键文本显示到文本框中
- ......
- 要阻止浏览器的默认行为,就需要在对应事件中加入以下代码:
// e为事件对象
e.preventDefault();
2)dom 尺寸和位置
- 尺寸 1
- 尺寸 2
- 获取元素的可见宽度:
dom.offsetWidth
- 可见宽度:边框、内边距、内容、滚动条
dom.clientWidth
不包含滚动条
- 获取元素的可见宽度:
- 尺寸 3
- 尺寸 4
相关信息
- 调用
dom.scrollTo(x, y)
可以设置元素的滚动位置
- x:scrollLeft
- y:scrollTop
该方法可用于使元素回到元素顶部
dom.scrollTo(0, 0)
如果要监听元素的滚动,可以监听事件类型:scroll
- Element.getBoundingClientRect()
- element-box-diagram
- top、left、right、bottom 均相对于视口
3)事件传播机制——事件流
// 在冒泡阶段触发
div.onclick = function () {};
// 在冒泡阶段触发事件(默认)
div.addEventListener("click", function () {});
div.addEventListener("click", function () {}, false);
// 在捕获阶段触发事件
div.addEventListener("click", function () {}, true);
- this
- 绑定当前事件的对象
- e.target
- 事件源,目标阶段的 dom
- 给 body 绑定事件,监听 button 的点击
- this 是 body,e.target 是 button
// 事件处理函数
function handler(e) {
console.log(e.target); // 获取事件源
console.log(this);
e.stopPropagation(); // 阻止事件继续冒泡
}
- 默认情况下,事件都会从目标源先触发,再冒泡到上层对象
- 与事件绑定顺序无关
var container = document.querySelector(".container");
var inner = document.querySelector(".inner");
var btn = document.querySelector("button");
container.addEventListener("click", function (e) {
console.log("container click!");
});
inner.addEventListener("click", function (e) {
console.log("inner click!");
});
btn.addEventListener("click", function (e) {
console.log("button click!");
});
/**
* button click!
* inner click!
* container click!
*/
冒泡机制可应用于事件委托
- 给多个元素绑定相同事件
- 全局只定义一个事件回调函数
- 后续新增的同类元素自动绑定相同事件
var container = document.querySelector(".container");
container.onclick = function (e) {
if (e.target.tagName === "BUTTON") {
console.log(e.target.innerText);
}
};
(六)技巧
1.在循环中注册事件
1)使用立即执行函数保存外部循环变量
for (var i = 0; i < 3; i++) {
// 产生一个新的作用域,作用域中有一个变量,值和这一次循环的i相同
(function (i) {
setTimeout(function () {
console.log(i);
}, 1000);
})(i);
}
2)为多个按钮注册事件
- 同 ES6 的 let 一样
- 原理:产生一个新的作用域
var btns = document.querySelectorAll("button");
for (var i = 0; i < btns.length; i++) {
(function (i) {
btns[i].onclick = function () {
console.log(i);
};
})(i);
}
// 等价于
// for (let i = 0; i < btns.length; i++) {
// btns[i].onclick = function () {
// console.log(i);
// };
// }
2.回调函数
- 函数执行后自动调用的某些函数
- 回调函数内部的 this 指向触发时的事件绑定的对象
dom.addEventListener("click", function (e) {
console.log("这是一个回调函数");
// this指向dom
});
3.函数防抖
- 解决问题:
- 事件触发后的回调函数非常耗时
- 事件触发过程中执行回调函数没有意义,只有触发动作结束时触发才有意义
- 以上条件均满足时应使用函数防抖
- 应用场景:
- 输入事件
- 点击事件
- 鼠标移动事件
- 尺寸变化事件
/**
* 防抖
* @param {function} fn 有耗时操作的回调函数
* @param {number} duration 延迟执行的时间
* @returns 没有耗时操作的回调函数
*/
function debounce(fn, duration) {
var timerId;
return function () {
clearTimeout(timerId);
// 将该函数的this传递到fn
var curThis = this;
// 将该函数的参数全部传递给fn => 将伪数组arguments切割成真数组
var args = Array.prototype.slice.call(arguments, 0);
timerId = setTimeout(function () {
fn.apply(curThis, args);
}, duration);
};
}
// input输入事件
var newHandler = debounce(function (e) {
console.log("用户有按键", e, this.value, "耗时操作");
}, 2000);
var inp = document.querySelector("input");
inp.addEventListener("input", newHandler);
// mousemove鼠标移动事件
var mouseMoveHandler = debounce(function () {
console.log("move");
}, 1000);
window.addEventListener("mousemove", mouseMoveHandler);