前言
-
给孩子点点关注吧!😭
- 本篇文章主要记录以下几部分:
- 进阶:
- 作用域;
- 函数进阶(函数提升、函数参数、箭头函数);
- 解构赋值;
- 对象进阶(构造函数、实例成员、静态成员);
- 内置构造函数(Object、Array、String、Number);
- 编程思想;
- 构造函数;
- 原型
- 高级:
- 深浅拷贝;
- 异常处理;
- this指向;
- 性能优化(防抖、节流);
- 进阶:
- 以下部分请移步JavaScript - 基础+WebAPI(笔记)
- 基础:
- 输入输出语法;
- 数据类型;
- 运算符;
- 流程控制 - 分支语句;
- 流程控制 - 循环语句;
- 数组 - 基础;
- 函数 - 基础;
- 对象 - 基础;
- Web API:
- DOM;
- DOM事件基础(事件监听、常用事件、事件对象);
- DOM事件进阶(事件流、事件委托);
- 日期对象;
- 节点操作;
- M端事件;
- JS插件;
- window对象;
- 本地存储;
- 正则表达式;
- 基础:
叁、❗❗ JavaScript进阶
一、 作用域
- 作用域(scope) = 全局作用域(Global) + 局部作用域(Local)
- 局部作用域 = 函数作用域 + 块作用域
-
作用域: 规定了变量 能够 被访问 的 范围,离开了这个范围,变量便不能被访问
- 变量的作用范围
1.1 局部作用域
Local
- 局部作用域 = 函数作用域 + 块作用域
1.1.1 函数作用域
- ⚠ 注意:
- 在 函数内部 声明的 变量 只能在 函数内部 被 访问,外部 无法直接访问(可以利用闭包来访问)
- 函数 的 参数 也是 函数内部 的 局部变量
- 不同函数 内部声明 的 变量 无法 互相访问
- ⚠ 函数执行完毕后,函数内部的变量实际被清空了(JS垃圾回收机制)
1.1.2 块作用域
- 在JavaScript中使用
{}
包裹的 代码 称为 代码块,代码块内部 声明的变量 外部【有可能:var声明的可以】无法访问- 代码展示:
// 用 let 声明的变量 有 块作用域 for (let i = 0; i < 5; i++) { console.log(i); } console.log(i); // i is not defined ------------------------------------------------------------------- // 用 var 声明的变量 没有 块作用域 for (var i = 0; i < 5; i++) { console.log(i); } console.log(i); // 5
- 代码展示:
- ⚠ 注意:
- ⚠
let
和const
声明的变量 会产生 块作用域 - ⚠
var
🔺不会产生🔻 块作用域- var 会被 函数作用域 所限制
- 不同代码块之间 的 变量 无法 互相访问
- 推荐使用
let
或const
- ⚠
1.2 全局作用域
Global
- ⚠ 注意:
- 为
window
对象 动态添加的 属性 默认 是 全局变量,不推荐! - 函数中 未使用 任何关键字 声明的 变量 是 全局变量,不推荐!!!
- 尽可能减少声明全局变量,防止变量污染
- 为
1.3 作用域链
-
作用域: 规定了变量 能够 被访问 的 范围,离开了这个范围,变量便不能被访问
- 变量的作用范围
- 嵌套关系 的 作用域 串联 起来形成了 作用域链
-
本质: 底层 变量 的 查找机制
- 在函数执行时,会 优先 在 当前 函数作用域 中 查找变量
- 如果 当前作用域 查找不到 则会 依次逐级 查找 父级作用域 直到 全局作用域
- 代码展示:
// 作用域链 // 本质上就是底层(最小的那个作用域) 变量的查找机制 // 变量访问机制:就近原则,由内向外 // 全局作用域 let a = 1; let b = 2; // 局部作用域 function f() { let a = 1; let b = 3; // 局部作用域 function g() { a = 2; let c = 4; console.log(a); } g(); // 调用g } f(); //调用f // 2
- ⚠ 注意:
- 相同作用域链 中按照 从小到大 的规则查找变量
- ⚠🔺 子作用域 能够访问 父作用域,父作用域 无法访问 子作用域
- 见上面代码展示:g函数 可以 log b,f函数 无法 log c
1.4 JS垃圾回收机制 - GC
- JS中 内存的分配 和 回收 都是 自动完成 的,内存 不使用 的时候 会被 垃圾回收器 自动回收
- 内存泄漏: 不再用到的内存,没有及时释放
-
内存的生命周期
- 1️⃣ 内存分配: 声明 变量、函数、对象的时候,系统会 自动 为它们 分配内存
- 2️⃣ 内存使用: 既 读写内存,也就是 使用 变量、函数等
- 3️⃣ 内存回收: 使用完毕,由垃圾回收器 自动回收 不再使用的内存
- ⚠ 注意:
- 全局变量 一般 不会回收(关闭页面回收)
- ⚠ 一般情况下 局部变量 的值,不用了,就会被自动回收
-
垃圾回收算法说明:
- 1️⃣❌ 引用计数法
- 致命缺陷:嵌套引用
- 如果两个对象相互引用,尽管他们不再使用,垃圾回收器不会进行回收,导致内存泄漏
- 2️⃣ 标记清除法
- 1️⃣❌ 引用计数法
1.5 ❗❗ 闭包
-
闭包:
Closure
-
概念: 一个函数 对 周围状态的引用 捆绑在一起,内层函数 能够访问到 其外层函数 的 作用域
-
闭包是一种使用过程
-
闭包 = 内层函数 + 外层函数的变量
-
代码展示:
function fn() { let b = 9; function outer() { const a = 1; console.log(b); function f() { console.log(a); console.log(b); } f(); } outer(); } fn();
- 函数套函数 不一定会 产生 闭包
- 如果 内层函数 用到 外层函数 的 变量 这种情况就 会产生闭包
-
-
⚠ 闭包使用注意: 闭包使用的时候,内部的变量因为被外部引用了,所以代码执行完毕不会释放内存 - 内存泄漏(引用计数法)
-
❗ 闭包作用:
- 实现数据的私有化
- 外部 可以 访问 函数内部 的 变量
- 允许将 函数 与其所操作的 某些数据(环境)关联起来
-
应用场景:
- 防抖
- 节流(定时器)
-
基本格式:
function fn() { let b = 9; function outer() { const a = 1; console.log(b); function f() { let c = 6; console.log(a); console.log(b); } return f; // function 复杂数据类型,返回出去的是指向函数的地址 } return outer; } const fun = fn(); // fn() === outer === function outer() {} => fun fun(); // b = 9 // 相当于调用outer()函数 const fun1 = fun();// fun() === outer() === f === function f() {} => fun1 // b = 9 fun1(); // a = 1; b = 9 // 相当于调用f()函数
-
闭包应用: 实现 数据 的 私有文章来源:https://www.toymoban.com/news/detail-444621.html
- 代码展示:
// 闭包的应用:统计函数调用的次数 function fun() { let i = 0; function fn() { i++; console.log(`fn()函数被调用了${i}次`); } return fn; } const f = fun(); // fun() === fn === function fn() {} => f f(); // Global -> fun() === fn === function fn() {} -> f -> fn() -> i++ -> i // 可以查找到i,所以i不会被回收,调用完函数后,i应该被回收,现在没有回收,内存泄漏
- 代码展示:
-
❗❗ 闭包面试:怎么理解闭包 -> 闭包的作用 -> 闭包可能引起的问题文章来源地址https://www.toymoban.com/news/detail-444621.html
1.6 ❌ 变量提升
- 它允许在变量声声明之前既被访问(仅存在于var声明变量)
- 变量提升: 在代码执行之前,检测在 当前作用域 下 所有 用 var 声明的变量,会将所有 用 var声明的变量 提升到 🔺当前作用域🔺 的 最前面
- ⚠ 注意:
- 变量提升出现在 相同作用域 当中
-
let
和const
声明的变量 不存在变量提升 - 只 提升 声明,不 提升 赋值
- 代码展示:
console.log(`${num}件`); var num = 10; // 控制台打印: undefined件 // 上面代码本质上就是下面的代码 var num; console.log(`${num}件`); num = 10;
-
ES6定义变量
let : 变量 const : 常量 let/const 和 var 的区别 + 预解析 -> let/const 不会进行预解析(变量提升),必须先定义后使用 -> var 会进行预解析 + 变量名 -> let/const 不能声明重复的变量名 -> var 可以声明重复的变量名 + 块级作用域(被代码块限制变量的使用范围) -> let/const 有块级作用域 (只要是能书写 {} 的代码块都能限制使用范围) -> var 没有块级作用域 (只有函数作用域才能限制使用范围) let 和 const 的区别 + let 是 变量 const 是 常量 + let 在声明的时候可以不进行赋值 (留着以后用) const 在声明的时候必须进行赋值 (声明不赋值会报错) + let 声明的变量可以被修改 const 声明的常量不可以修改 (修改就会报错)
二、函数进阶
2.1 函数提升
- 代码执行之前,会把所有 函数声明 提升到 当前作用域 的 最前面
- 预解析 === 声明提升:在每个作用域的 代码执行之前,会把当前作用域代码中的声明部分提升到作用域最前面执行
- 变量声明提升(var)
- 函数声明提升(function)
- ⚠ 注意:先提升var再提升function
- ⚠🔺 注意:
- 🔺 只 提升 函数声明,不 提升 函数调用
- 🔺 函数表达式 必须 先声明 后调用(🔺不存在函数提升🔺)
- 函数提升 优先级 高于 变量提升
- 代码展示:
fn(); function fn() { console.log('函数提升,只提升声明,不提升调用'); // 上面代码本质上就是下面代码 function fn() { console.log('函数提升,只提升声明,不提升调用'); } fn(); ---------------------------------------------------------------------- fun(); var fun = function () { console.log('函数表达式不存在提升现象'); } // 报错:Uncaught TypeError(未捕获的类型错误): fun is not a function var fun; // fun -> fun(); fun = function () { console.log('函数表达式不存在提升现象'); } // fun不能提升, // 上面代码是在给fun赋值,var声明的变量 只 提升声明,不 提升赋值
2.2 函数参数
- 函数参数默认值 + 动态参数 + 剩余参数
2.2.1 动态参数
- arguments: 函数内部 内置 的 伪数组变量,包含了 调用 函数时 传入 的 所有实参
- 只能在普通函数里使用
- 作用: 动态获取函数的实参
- ⚠ 注意:
- 表示 所有实参 的 集合
- arguments 是一个 伪数组, 只存在于 函数内部
- 箭头函数 🔺没有🔺 arguments
- 🔺 可读写
- 代码展示:
// 动态参数 - arguments(伪数组) function getSum() { let sum = 0; for (let i = 0; i < arguments.length; i++) { sum += arguments[i]; } return sum; } console.log(getSum(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)); // 55
2.2.2 ✔ 剩余参数
- 允许将一个 不定数量 的 参数 表示为一个 数组
- … 是语法符号,置于 最末 函数形参 之前,用于获取 剩余(多于) 的实参
- ⚠ 注意:
- 借助 … 获取的 剩余实参,是个真数组
- 剩余参数放在 最末位
- 如果 没有剩余的参数,得到的就是一个 空数组
- 代码展示:
// 剩余参数 - ...变量名(数组) function getSum(...arr) { console.log(arr); } getSum(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); // [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] ---------------------------------------------------------------------- // ... 是语法符号,置于 最末 函数形参 之前,用于获取 剩余(多余) 的实参 function getSum(a, b, c, ...arr) { console.log(arr); } getSum(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); // [4, 5, 6, 7, 8, 9, 10]
-
动态参数 和 剩余参数 的区别:
- 动态参数 是 伪数组
- 剩余参数 是 真数组
-
拓展:
-
展开运算符:(…)
- 展开运算符将一个 数组、对象 展开
- 不改变原始数组
- 当你在 函数的实参 或者 数组 或者 对象 里面使用的时候是 展开运算符
- 展开运算符写在数组前面
- 应用场景:
- 用于获取数组的最大值
- 合并数组
- 解构赋值(部分场景)
- 解构赋值 中 展开运算符 只能写在 最后一个元素 的 前面
- 展开运算符 or 剩余参数
- 展开运算符: 数组 中使用,数组展开,在 数组内部 使用
- 剩余参数: 函数参数 使用,得到真数组,在 函数内部 使用
- 代码展示:
// 获取数组最大值 const arr = [3, 45, 2, 89, 54]; console.log(...arr); // 3 45 2 89 54 console.log(Math.max(...arr)); // 89 // 合并数组 const arr1 = [1, 2, 4]; const arr2 = [...arr, ...arr1]; // ...arr1 === 1, 2, 4 console.log(arr2); // [3, 45, 2, 89, 54, 1, 2, 4] // 解构赋值 const [n1, n2, ...n3] = [1, 2, 3, 4, 5]; console.log(n1, n2, n3); // 1 2 [3, 4, 5] --------------------------------------------------------------- const [n1, ...n2, n3] = [1, 2, 3, 4, 5]; // Uncaught SyntaxError: Rest element must be last element
- 展开运算符将一个 数组、对象 展开
-
展开运算符:(…)
2.3 ❗❗❗ 箭头函数
- ⚠🔺 注意:
- 箭头函数 没有 arguments,但是 有 剩余参数
- 箭头函数 没有 this
- 箭头函数 替代原本需要 匿名函数 的地方
- 函数表达式 的 简写方式 (匿名函数)
- 声明式函数 不能写
2.3.1 基本语法
-
语法:
() => {} // (function () {}) () : 形参的位置 => : 箭头函数的标志 {} : 代码段
- ⚠🔺 特性:
- 1️⃣ 只有 一个形参
- 可以 省略小括号
- 代码展示:⬇
- 2️⃣ 代码段 只有 一行代码,并且 是 返回值
- 可以 省略 大括号,并 自动 做为 返回值 被返回
- 如果有返回值,可以 省略 return
- 代码展示:⬇
- 3️⃣ 加括号 的 函数体 返回 对象字面量表达式(直接返回一个对象)
- 箭头函数可以返回对象,但是必须用小括号包裹
- 代码展示:⬇
-
代码展示:
// 箭头函数只有一个参数可以省略小括号 const fn = x => { console.log(x); } fn(1); // 1 // 箭头函数没传递参数小括号还是要写的 const fn1 = () => console.log('箭头函数没传递参数小括号还是要写的'); fn1(); // '箭头函数没传递参数小括号还是要写的' // 箭头函数代码段只有一行代码,可以省略大括号 const fn2 = x => console.log(x); fn2(2); // 2 // 箭头函数只有一行代码,且有返回值,可以省略return const fn3 = x => x + x; console.log(fn3(3)); // 6 // 加括号的函数体返回对象字面量表达式(直接返回一个对象) // 对象的{} 和 函数的{}有冲突,需要拿个 () 包住对象 const fn4 = uname => ({ uname: uname }); console.log(fn4('迪迦奥特曼')); // {uname: '迪迦奥特曼'}
- 1️⃣ 只有 一个形参
- ⚠ 注意:
- ⚠🔺箭头函数 属于 表达式函数,因此 不存在函数提升
2.3.2 箭头函数参数
- 箭头函数 没有 arguments 动态参数,但是 有 剩余参数 …args
- 代码展示:
const getSum = (...args) => { let sum = 0; args.forEach(item => { sum += item; }); return sum; } console.log(getSum(3, 9)); // 12
- 代码展示:
- 箭头函数只要你设置默认值,不管多少个形参,都必须要写小括号
let fn = (a = 100) => { console.log(a); } fn(); // 10 fn(100); // 100
2.3.3 箭头函数 this
- 函数 才有this
- 以前的this指向:
// 以前this指向:谁调用这个函数 this 指向谁 console.log(this); // this -> window // window.console.log(this); // 普通函数的this function fn() { console.log(this); } fn(); // this -> window // window.fn(); // 对象方法里面的this const obj = { uname: '迪迦奥特曼', sayHi: function () { console.log(this); } } obj.sayHi(); // this -> obj
- window 是JS中的全局对象,我们 声明 的 变量 或 函数 实际上是给 window 添加 属性 或 方法
-
箭头函数 不会创建 自己的this,它只会 沿用 自己所在这条作用域链 的 上一层作用域 的 this
- 箭头函数的 this指向被创建的时候上下文中的this(出生的时候,所在的作用域中的this是谁,以后就都指向谁)
- 箭头函数里面的 this 任何方法都改变不了
- 因为箭头函数没有 this (它用的是上一层作用域链的 this )
- call / apply / bind 不能改变
- 代码展示:
// 箭头函数没有this指向,它只会 沿用 自己所在这条作用域链 的 上一层作用域 的 this const fn = () => console.log(this); // this -> window fn(); // 并不是window调用了fn,而是箭头函数没有this,它只会沿用自己所在这条作用域链的上一层作用域的this,上一层作用域是Global,Global的this指向window,所以指向window const obj = { uname: '迪迦奥特曼', sayHi: () => console.log(this) // this -> window } obj.sayHi(); // window.obj.sayHi(); // 箭头函数没有this,它只会沿用自己所在这条作用域链的上一层作用域的this // 也就是obj的this,window调用了obj,所以this指向window const obj1 = { uname: '迪迦奥特曼', sayHi: function () { let i = 10; const count = () => console.log(this); // this -> obj1 count(); } } obj1.sayHi(); // 箭头函数没有this,它只会沿用自己所在这条作用域链的上一层作用域的this // 既function的this,function(普通函数)的this指向函数的调用者obj1这个对象
三、❗❗ 解构赋值
3.1 数组解构
3.1.1 基本语法
- 数组解构: 是 将数组的 单元值(数组元素) 快速 批量赋值 给 一系列变量 的 简介语法
-
基本语法:
- 赋值运算符 = 左侧 的
[]
用于 批量 声明变量,右侧数组 的 单元值 将被 赋值给左侧的变量 - 变量的顺序 对应 数组单元值的位置 依次 进行 赋值操作
- 赋值运算符 = 左侧 的
- ⚠🔺 注意: 数组解构 赋值 是按照 索引 赋值 的
- 代码展示:
// 将等号左侧数组里的单元值按顺序赋值给等号右侧的变量 // 按照索引进行赋值 const [a, b, c] = [1, 2, 3]; console.log(a); // 1 console.log(b); // 2 console.log(c); // 3
- 利用数组解构交换两个变量的值
let a = 11; let b = 22; [b, a] = [a, b]; console.log(a) // 22 console.log(b) // 11 ---------------------------------------------------------------------- let a = 11 let b = 22 [b, a] = [a, b] console.log(a) console.log(b) // Uncaught ReferenceError: b is not defined // let b = 22 => 后面必须加分号才不会报错
3.1.2 特殊情况
- 1️⃣ 变量多 单元值少
- 右侧的单元值按顺序对左侧的变量进行赋值,没有赋值的变量则是 undefined
- 代码展示:
const [hr, lx, mi, fz, hw] = ['海尔', '联想', '小米', '方正']; console.log(hr, lx, mi, fz, hw); // 海尔 联想 小米 方正 undefined
- 2️⃣ 变量少 单元值多
- 右侧的单元值按顺序对左侧的变量进行赋值,剩余的单元值丢弃不用
- 代码展示:
const [hr, lx, mi] = ['海尔', '联想', '小米', '方正']; console.log(hr, lx, mi); // 海尔 联想 小米
- 3️⃣ 利用 剩余参数 解决 变量少 的问题
- ⚠ 注意:
- 剩余参数 返回的是一个真数组
- 剩余参数 放在 最末位
- 代码展示:
const [hr, lx, ...mi] = ['海尔', '联想', '小米', '方正']; console.log(hr, lx, mi); // 海尔 联想 [小米, 方正]
- ⚠ 注意:
- 4️⃣ 防止 undefined 传递
- 设置默认值
- 代码展示:
const [hr, lx, mi, fz, hw = '华为'] = ['海尔', '联想', '小米', '方正']; console.log(hr, lx, mi, fz, hw); // 海尔 联想 小米 方正 华为
- 5️⃣ 按需导入,忽略某些值
- ⚠ 变量可以忽略,但是位置还是要留的
- 代码展示:
const [hr, lx, mi, , hw = '华为'] = ['海尔', '联想', '小米', '方正']; console.log(hr, lx, mi, hw); // 海尔 联想 小米 华为
- 6️⃣ 多维数组解构
- 代码展示:
const [a, [b, [c, d, [e]]], f] = [1, [2, [3, 4, [5]]], 6]; console.log(a, b, c, d, e, f); // 1 2 3 4 5 6
- 代码展示:
3.2 对象解构
- 对象解构: 是 将对象 属性 和 方法 快速 批量赋值 给一些列 变量 的 简介语法
3.2.1 基本语法
- 赋值运算符 = 左侧的
{}
用于 批量声明变量,右侧对象的属性值将被 赋值 给左侧的变量 - 对象属性的值 将被赋值给 与 对象属性名 相同的 变量
- ⚠ 注意:
- ⚠🔺 对象解构赋值的时候按照 属性名 进行 赋值
- 对象解构的变量名不要和外面的变量名冲突否则报错
- ⚠🔺 对象中 找不到 与 变量名一致的属性名 时 变量值 为 undefined
- ⚠🔺🔺 变量名 和 对象的属性名 必须一致,否则 变量的值 就是 undefined
- ⛔错误展示:
const { uname, Age } = { uname: '迪迦奥特曼', age: 22 }; console.log(uname, Age); // 迪迦奥特曼 undefined
- 代码展示:
const {uname, age} = {uname: '迪迦奥特曼', age: 22}; console.log(uname, age); // 迪迦奥特曼 22 // 等价于 // const uname = obj.uname; // const age = obj.age;
3.2.2 特殊情况
- 1️⃣ 对象解构的变量名 可以重新声明
- ⚠语法: 旧变量名: 新变量名
- 代码展示:
const uname = '赛罗奥特曼'; const { uname: username, age } = { uname: '迪迦奥特曼', age: 22 }; console.log(username, age); // 迪迦奥特曼 22
- 2️⃣ 解构数组对象
- 代码展示:
const obj = [ { uname: '迪迦奥特曼', age: 22 }, { uname: '赛罗奥特曼', age: 23 } ]; const [{ uname, age }, { uname: userName, age: userAge }] = obj; console.log(uname, age, userName, userAge); // 迪迦奥特曼 22 赛罗奥特曼 23
- 代码展示:
- 3️⃣ 多级对象解构
- ⚠ 解构的时候必须写 🔺对象名🔺,不能打印
- 代码展示:
const pig = { name: '佩奇', family: { mother: '猪妈妈', father: '朱爸爸', sister: '乔治' }, age: 6 }; const { name, family: { mother, father, sister }, age } = pig; console.log(name, mother, father, sister, age); // 佩奇 猪妈妈 朱爸爸 乔治 6
✔✔const person = [ { name: '佩奇', family: { mother: '猪妈妈', father: '朱爸爸', sister: '乔治' }, age: 6 } ]; const [{ name, family: { mother, father, sister }, age }] = person; console.log(name, mother, father, sister, age); // 佩奇 猪妈妈 朱爸爸 乔治 6
// 1. 这是后台传递过来的数据 const msg = { "code": 200, "msg": "获取新闻列表成功", "data": [ { "id": 1, "title": "5G商用自己,三大运用商收入下降", "count": 58 }, { "id": 2, "title": "国际媒体头条速览", "count": 56 }, { "id": 3, "title": "乌克兰和俄罗斯持续冲突", "count": 1669 }, ] }; // 需求1:请将以上msg对象,采用对象解构的方式,只选出 data 方便后面使用渲染页面 const { data } = msg; console.log(data); // 需求2:上面msg是后台传递过来的数据,我们需要把data选出当做参数传递给 函数 // const { data } = msg; // msg 虽然很多属性,但是我们利用解构只要 data值 function render({ data }) { // const { data } = arr; // 我们只要 data 数据 // 内部处理 console.log(data); } render(msg); // 需求3:为了防止msg里面的data名字混淆,要求渲染函数里面的数据名改为 myData function render({ data: myData }) { // 要求将 获取过来的 data数据 更名为 myData // 内部处理 console.log(myData); } render(msg);
- 今日案例拓展:
-
:active
伪类选择器- 活动链接(点击的时候就是活动链接)
-
draggable
可拖拽的-
draggable ="false"
禁止拖拽 -
draggable ="true"
可以拖拽
-
-
四、对象进阶
4.1 创建对象的三种方式
- 1️⃣ 字面量 创建
-
const obj = {};
-
- 2️⃣❌ new Object 创建 (系统的构造函数)
-
const obj = new Object();
- 追加属性
const obj = new Object({uname: '迪迦奥特曼', age: 22});
-
- 3️⃣ 工厂函数创建
- 4️⃣ 构造函数 创建 (自定义的构造函数)
-
new操作符的功能
- 在函数代码执行之前,隐式的创建一个空对象,把this指向空对象
- 执行函数代码
- 在函数代码执行之后,隐式的返回this
-
操作对象
- 1️⃣ 点语法 — 操作对象
// 增 o.name = 'Jack' o.age = 20 // 删 delete o.name // 查 console.log(o.age) // 改 o.age = 22
- 1️⃣ 点语法 — 操作对象
- 2️⃣ 数组关联语法 — 操作对象
增 o['name'] = 'Jack' o['age'] = 20 o['gender'] = '男' o['class'] = 1 删 delete o['name'] 查 o['age'] 改 o['age'] = 21
-
判断一个成员在不在对象里面 — in
-
语法:
属性名 in '对象名'
- 返回值: true / false
- 代码展示:
let obj = { name: 'jack', age: 18, gender: '男', score: 100 }; console.log('name' in obj); // true
-
语法:
4.2 构造函数(自封装)
- 是一种 特殊的函数,主要用来 初始化对象
- 可以用构造函数 创建多个 类似 对象(对公共的部分进行抽取并封装)
-
规范:
- 命名以 大写字母 开头
- ⚠🔺 只能由 new 操作符来执行
- ⚠🔺 注意:
- 使用 new 关键字调用函数的行为被称为 实例化
- 构造函数内部 无需写return,返回值 即为 新创建的对象
- 构造函数内部的 return 返回的值 无效,所以不要写 return
- ❗❗ 实例化执行的过程: (面试)
- 1️⃣ 创建新的 空对象
- 2️⃣ 构造函数 this 指向 新对象
- 3️⃣ 执行构造函数代码,修改this,添加新属性
- 4️⃣ 返回新对象
- 代码展示:
4.3 实例成员 & 静态成员
4.3.1 实例成员
- 构造函数 创建的 对象 称为 实例对象
- 🔺 实例对象 的 属性 和 方法 称为 实例成员
- ⚠ 注意:
- 为 构造函数 传入 参数,动态创建 结构相同 但 值不同 的 对象
- 构造函数 创建 的 实例对象 彼此独立 互不影响
4.3.2 静态成员
- 🔺 构造函数 的 属性 和 方法 被称为 静态成员
- ⚠🔺 静态成员方法 中的 this 指向 构造函数
- 一般 公共特征 的属性或方法 静态成员设置为静态成员
五、内置构造函数
- 字符串、数值、布尔等基本数据类型也都有专门的构造函数,称为 基本包装类型
- 引用类型: Object、Array、RegExp、Date 等
- 包装类型: String、Number、Boolean 等
5.1 Object
- ❌ 使用内置构造函数创建对象
const obj = new Object({uname: '迪迦', age: 22});
- 常用的 静态方法:
- 静态方法: 只有 构造函数 Object 才可以 调用 (写在构造函数身上的方法)
- 1️⃣ ✔ Object.keys()
- 获取当前对象中的 所有 可枚举的 属性名(键)
-
语法:
Object.keys(对象名)
- ⚠ 返回值: 数组 (数组的元素 = 对象的属性)
- 代码展示:⬇
- 2️⃣ ✔Object.values()
- 获取对象中 所有 的 属性值
-
语法:
Object.values(对象名)
- ⚠ 返回值: 数组(数组的元素 = 对象属性值)
- 代码展示:⬇
- 3️⃣ Object.assign()
- 用于 对象拷贝(浅拷贝)
- 使用场景: 给对象添加属性
-
语法:
Object.assign(b, a) // 把 a 拷贝给 b Object.assign(拷贝者, 被拷贝者);
- 代码展示:⬇
- 代码展示:
// 静态方法:只有 构造函数 Object 才可以调用 const obj = { uname: '迪迦', age: 22 }; // Object.keys(对象):获取对象的 所有属性名,放在一个数组里面返回 const arr = Object.keys(obj); console.log(arr); // ['uname', 'age'] // Object.values(对象名):获取对象的 所有属性值,放在一个新数组中返回 const arr1 = Object.values(obj); console.log(arr1); // ['迪迦', 22] // Object.assign(拷贝者, 被拷贝者):将对象a拷贝给对象b const obj1 = {}; Object.assign(obj1, obj); console.log(obj1); // { uname: '迪迦', age: 22 } // 追加属性 Object.assign(obj1, {gender: '男'}); console.log(obj1); // {uname: '迪迦', age: 22, gender: '男'}
5.2 Array
- ❌使用Array内置构造函数创建数组
const arr = new Array(); // 创建一个空数组 const arr = new Array(5); // 一个参数:数组的长度 const arr = new Array(1, 2, 3, 4, 5); // 多个参数:[1, 2, 3, 4, 5]
5.1 数组常用方法总结
5.1.1 改变原始数组 (7)
- 1️⃣ push()
- 描述: 将 一个或多个元素 追加到 数组的末尾 ,并返回 追加元素之后 数组的 length
-
语法:
arr.push(elmeent1, ..., elementN);
-
返回值:
- ⚠ 追加元素 之后 数组的 length
- 代码展示:
const arr = [1, 2, 3]; const length = arr.push(4, 5); // 追加元素之后arr的length console.log(arr); // [1, 2, 3, 4, 5] console.log(length); // 5
- 2️⃣ unshift()
- 描述: 将 一个或多个元素 插入到 数组的开头 ,并返回 插入元素之后 数组的 length
-
语法:
arr.unshift(element1, ..., elementN);
-
返回值:
- ⚠ 插入元素 之后 数组的 length
- 代码展示:
const arr = [3, 4, 5]; const length = arr.unshift(1, 2); // 插入元素之后arr的length console.log(arr); // [1, 2, 3, 4, 5] console.log(length); // 5
- 3️⃣ pop()
-
描述: 删除 数组 最后一个元素,并返回 被删除的元素
- ⚠ 会 改变 数组的 length
-
语法:
arr.pop();
-
返回值:
- 被删除的元素
- 代码展示:
const arr = [1, 2, 3]; const lastValue = arr.pop(); // 被删除的元素 console.log(arr); // [1, 2] console.log(lastValue); // 3
-
描述: 删除 数组 最后一个元素,并返回 被删除的元素
- 4️⃣ shift()
-
描述: 从数组中删除 第一个元素,并返回 该元素的值
- 改变数组的长度
-
语法:
arr.shift()
- 返回值: 被删除的元素
- 代码展示:
const arr = [1, 2, 3]; const firstValue = arr.shift(); // 被删除的元素 console.log(arr); // [2, 3] console.log(firstValue); // 1
-
描述: 从数组中删除 第一个元素,并返回 该元素的值
- 5️⃣ splice()
- 描述: 通过 删除 或 替换 现有元素 或 原地添加新元素 来 修改数组,并以 数组的形式返回被修改的内容
-
语法:
array.splice(start[, deleteCount[, item1][, ...])
- 返回值: 以 数组的形式 被修改 的 内容
-
注意:
- ⚠ 删除/替换 的时候,包括 开始索引 的 位置
- ⚠ 添加 的时候,是添加在 开始索引位置之前
- 添加 的时候返回的是 空数组
- 代码展示:
// 删除 const arr = [1, 2, 3]; const Value = arr.splice(1, 1); // 以数组的形式返回被修改的内容 console.log(arr); // [1, 3] console.log(Value); // [2] // 替换 const arr = [1, 2, 3]; const Value = arr.splice(1, 1, 6); // 以数组的形式返回被修改的内容 console.log(arr); // [1, 6, 3] console.log(Value); // [2] // 添加 const arr = [1, 2, 3]; const Value = arr.splice(1, 0, 8); // console.log(arr); // [1, 8, 2, 3] console.log(Value); // []
- 6️⃣ sort() - 数组排序
- 描述: 对数组元素进行排序,并返回数组
-
语法:
arr.sort() 可以省略参数,升序 arr.sort(function (a, b) { return a - b } ) 升序排列 arr.sort(function (a, b) { return b - a } ) 降序排列
- 返回值: 排序好的数组
- 代码展示:
const arr = [23, 45, 78, 10]; arr.sort(); console.log(arr); // [10, 23, 45, 78] arr.sort(function (a, b) {return a - b}); console.log(arr); // [10, 23, 45, 78] arr.sort(function (a, b) {return b - a}); console.log(arr); // [78, 45, 23, 10]
- 7️⃣ reverse() - 反转数组
- 描述: 将数组中的元素的 位置颠倒 ,并返回该数组
-
语法:
arr.reverse();
- 返回值: 颠倒顺序后的数组
- 代码展示:
const arr = [1, 2, 3, 4, 5]; arr.reverse(); console.log(arr); // [5, 4, 3, 2, 1]
5.1.2 不改变原始数组
- 1️⃣ ✔ forEach - 遍历数组
- 描述: 适合遍历 数组对象
-
语法:
Array.forEach(function (item[, index[, Array]]) { } )
- 返回值: 无
- 代码展示:
const arr = ['red', 'purple', 'pink']; arr.forEach((item, index) => { console.log(item); console.log(index); }); // red 0 purple 1 pink 2
- 2️⃣ ✔ map() - 迭代数组(映射数组)
- 描述: 遍历原数组,把原数组中的每一个数据加工改造,形成一个新数组返回
-
语法:
Array.map(function (item[, index[, Array]]) {} )
-
返回值: 新数组
- 将原始数组里面的元素进行处理之后添加到新数组里
- 新数组length = 旧数组length
- 代码展示:
const arr = ['red', 'blue', 'green']; const newArr = arr.map(function (item, index, arr) { return item + '老师'; }); console.lgo(newArr); // ['red老师', 'blue老师', 'green老师'] const arr1 = [10, 20, 30]; const newArr1 = arr1.map(function (item, index, arr) { return item + 10; }); console.log(newArr1); // [20, 30, 40]
- 3️⃣ ✔ filter() - 筛选数组
-
描述: 过滤 原始数组中的数据,把 满足条件 的数据放在一个新数组中
- 在执行函数返回true的情况下,留下该数据,返回为false去除该数据
-
语法:
Array.filter(function (item[, index[, Array]]) {} )
- 返回值: 是一个新数组,里面是所有原始数组中满足条件的数据
- 代码展示:
const arr = [20, 1, 34, 60, 49, 90, 200, 288]; const newArr = arr.filter(item => { return item > 100; }); console.log(newArr); // [200, 288] ------------------------------------------------------------------- const arr = [23, 4, 19, 22, 56, 77]; const newArr = arr.filter(item => { if (item % 2 === 0) { return true; } else { return false; } }); console.log(newArr); // 上面代码等价于下面代码 const newArr = arr.filter(item => item % 2 === 0); console.log(newArr);
-
描述: 过滤 原始数组中的数据,把 满足条件 的数据放在一个新数组中
- 4️⃣ ✔ reduce() - 累计器(求和)
- 描述: 每一次运行 reduce 会将先前元素的计算结果作为参数传入,最后将其结果汇总为单个返回值
-
语法:
Array.reduce(function () {}, 起始值) Array.reduce(function (累计值, 当前元素[, 索引号][, 原数组]) {}[, 起始值])
-
参数:
- 起始值可以省略
- 如果写就作为第一次累计的起始值
- 如果没有,就取第一个元素作为起始值(累加的时候从第二个元素开始)
- 有起始值,则以起始值为准开始累计,累计值 = 起始值
- 没有起始值,则累计值以数组的第一个元素作为起始值开始累计
- 后面每次遍历就会用后面的数组元素 累计 到 累计值 里面
- 起始值可以省略
- 返回值: 返回函数累计的处理的结果
- 代码展示:
// 数组.reduce(function (累计值, 当前元素) {}, 起始值) const arr = [1, 2, 3, 4, 5]; const sum = arr.reduce((prev, item) => prev + item, 0); // prev + item // 0 + 1 = 1 // 1 + 2 = 3 // 3 + 3 = 6 // 6 + 4 = 10 // 10 + 5 = 15 console.log(sum); // 15 const sum = arr.reduce((prev, item) => prev + item); // 将数组的第一个元素给prev,从第二个元素开始累加 // prev + item // 1 + 2 = 3 // 3 + 3 = 6 // 6 + 4 = 10 // 10 + 5 = 15 console.log(sum); // 15
- 5️⃣ join()
-
描述: 将一个 数组 或 类数组对象 的所有元素连接成一个 字符串,并返回这个字符串
- 注意: 将 所有的元素 转换成 字符串,再 用 分隔符 将这些字符串 连接 起来
-
语法:
arr.join([分隔符]);
- 参数: 默认 使用 逗号
- 返回值: 字符串
- 代码展示:
const arr = ['迪迦', '赛罗', '泰罗']; let str = arr.join(); let str1 = arr.join('-'); console.log(str); // 迪迦,塞罗,泰罗 console.log(str1); // 迪迦-塞罗-泰罗 console.log(arr); // ['迪迦', '赛罗', '泰罗']
-
描述: 将一个 数组 或 类数组对象 的所有元素连接成一个 字符串,并返回这个字符串
- 6️⃣ ✔find() - 返回数组中满足条件的 第一个元素
- 描述: 返回 数组 中 满足条件 的 第一个元素
-
语法:
Array.find(function (item, index, arr) {} [, thisArg])
-
返回值:
- 有 满足条件 元素: 返回 第一个 元素
- 没有 这个元素: undefined
- 代码展示:
const arr = [5, 12, 8, 130, 44]; const found = arr.find(item => item > 10); console.log(found); // 12
- 7️⃣ some()
- 描述: 判断 数组中是不是 至少有一个元素 满足条件
-
语法:
Array.some(function (item, index[, arr]) {})
-
返回值: 布尔值(true / false)
- 有 一个 满足 - true
- 都不 满足 - false
- 代码展示:
const arr = [1, 2, 3, 4, 5]; const value = arr.some(item => item >= 3); console.log(value); // true
- 8️⃣ ✔every()
- 描述: 判断 数组 里面的 每一个元素 是不是都 满足条件
-
语法:
Array.every(function (item, index[, arr]) {})
-
返回值: 布尔值(true / false)
- 都满足 - true
- 有 一个 不满足 - false
- 代码展示:
const arr = [2, 26, 56, 34, 67]; let flag = arr.every(item => item > 30); console.log(flag); // false
- 9️⃣ concat() - 合并数组
- 描述: 合并 两个或多个数组。此方法不会更改现有数组,而是返回一个 新数组
-
语法:
const new_array = oldArr.concat(value1[, value2[, ...[, valueN]]])
-
参数:
- 将参数按顺序追加到数组末尾
- 返回值: 合并之后 的 新数组
- 代码展示:
// 都是数组 const arr = [1, 2, 3]; const arr1 = [4, 5, 6]; const new_array = arr.concat(arr1); console.log(new_array); // [1, 2, 3, 4, 5, 6] // 参数不是数组 const num = 8; const new_array = arr.concat(num); console.log(new_array); // [1, 2, 3, 8] // 参数是对象 const obj = {uname: '奥特曼', age: 22}; const new_array = arr.concat(obj); console.log(new_array); // [1, 2, 3, { uname: "奥特曼", age: 22 }]
- 🔟 slice() - 提取元素
- 描述: 获取指定的元素
-
语法:
Array.slice(开始索引, 结束索引)
- ⚠ 参数:
- 包前不包后
- 第一个参数不写: 头 ➡ 指定位置
- 第二个参数不写: 指定位置 ➡ 尾
- 参数可以是一个负数 -> length + 负数
- ⚠ 参数:
- 返回值: 一个含有被提取元素的 新数组
- 代码展示:
const arr = [1, 2, 3, 4, 5, 6]; const newArr = arr.slice(2, 4); // [3, 4] // 第一个参数不写 const newArr = arr.slice(4); // [5, 6] // 第二个参数不写 const newArr = arr.slice(2); // [3, 4, 5, 6] // 参数是负数 const newArr = arr.slice(2, -2); // [3, 4] // 不写参数 const newArr = arr.slice(); // [1, 2, 3, 4, 5, 6] console.log(newArr);
-
数组降维(数组扁平化)
arr.flat(几维数组); arr.flat(Infinity); // 一劳永逸,不管几维数组都能降到一维数组
-
正向 查看数组里面 指定这个 数据的索引
Array.indexOf(数据) Array.indexOf(数据, 开始索引) 从哪个索引开始向后查找 返回值: 如果有这个数据,是第一个满足条件的数据的索引 如果没有这个数据,就是 -1
-
反向 查看数组里面 指定数据 的索引
Array.lastIndexOf(数据) Array.lastIndexOf(数据, 开始索引) 注意: 虽然是反向查找,但是索引还是正常索引
-
使用 数组里面的内容 替换 数组里面的内容
Array.copyWithin(目标位置, 开始索引, 结束索引) ->目标位置:当你替换内容的时候,从哪一个索引位置开始替换 ->开始索引:数组哪一个索引位置开始当作替换内容,默认是 0 ->结束索引:数组哪一个索引位置结束当作替换内容,默认是 结尾 返回值: 是一个新数组(替换后的数组)
-
使用 指定数据区 填充 数组
Array.fill(要填充的数据, 开始索引, 结束索引) 前提: 数组要有 length 返回值: 填充好的数组
-
查看 数组中是不是有 某一个数据
Array.includes(数据) 返回值: 一个布尔值 有这个数据就是 true 没有这个数据就是 false
-
根据条件找到数组里面满足条件的数据的索引
Array.findIndex(function (item) {} ) 返回值: 找到满足条件的第一个元素的索引
5.2 伪数组 ➡ 真数组
-
静态方法:
-
语法:
Array.from()
- 代码展示:
// Array.from(伪数组):将伪数组转换为真数组 // 返回值:真数组 const arr = Array.from(document.querySelectorAll('li')); arr.pop(); console.log(arr);
-
语法:
- 伪数组(对象)
- 有索引值
- 有length属性
- 有 索引值 并且 有 length属性 的 对象 就是 伪数组
-
const obj = { 0: 'zs', 1: 20, 2: '小妹', length: 3 } console.log(Array.from(obj)); // ['zs', 20, '小妹']
5.3 String
- ❌ 字符串的创建
字面量创建 let str = 'hello world' 内置构造函数创建 let str = new String('hello world')
5.3.1 字符串方法
- 都是 实例方法
- ⚠ 注意: 所有字符串方法 都 不会改变 原始字符串
- 1️⃣ ✔
substring()
- 截取字符串-
语法:
str.substring(indexStart[, indexEnd])
-
参数:
- indexStart = indexEnd ➡ 返回一个 空字符串
- 省略indexEnd ➡ 提取字符 一直 到 字符串末尾
- 如果任一 参数小于0或为NaN,则被 当作 0
- 如果任一 参数大与 string Name.length,则被当作 stringName.length
- 如果 indexStart 大与 indexEnd,则substring 的执行结果就像两个参数调换了一样。
- 返回值: 指定部分 的 新字符串 (截取的部分)
- ⚠ 注意: 包前 不包后
- 代码展示:
// 截取字符串 // str.substring(indexStart[, indexEnd]); // 返回截取的部分 const str = '迪迦奥特曼,塞罗奥特曼,银河奥特曼,艾克斯奥特曼'; // 正常索引值(包前不包后) const newStr = str.substring(4, 9); // 曼,塞罗奥 // 一个参数 const newStr = str.substring(6); // 塞罗奥特曼,银河奥特曼,艾克斯奥特曼 // indexStart = indexEnd const newStr = str.substring(6, 6); // 空字符串 // 参数小于0或者为NaN const newStr = str.substring(-9); // 迪迦奥特曼,塞罗奥特曼,银河奥特曼,艾克斯奥特曼 const newStr = str.substring(NaN); // 迪迦奥特曼,塞罗奥特曼,银河奥特曼,艾克斯奥特曼 // 参数大与stringName.length const newStr = str.substring(100); // 空字符串 // indexStart>indexEnd const newStr = str.substring(9, 4); // 曼,塞罗奥 console.log(newStr);
- ❌ 拓展:也是截取字符串的方法
- substr()
- slice()(弃用)
-
语法:
- 2️⃣ ✔
split()
- 将 字符串 拆分成 数组 (分割)- 字符串中截取从开始下标到的指定数目的字符
-
语法:
str.split(分隔符)
-
参数:
- 字符串里面用的什么分隔符参数就是什么
- 没有参数 / 别的字符: 数组里面的 元素 就是 字符串本身
- 空字符串: 将字符串的每一个 字母 或 数组 或 文字 或 标点符号 都作为一个 数组元素
- 返回值: 数组
- 注意: 和 数组的join方法 相反
- 代码展示:
// String.split():将字符串转换成数组 // 字符串用什么分隔参数就是什么 const str = '迪迦奥特曼,塞罗奥特曼,银河奥特曼,艾克斯奥特曼'; const arr = str.split(','); console.log(arr); // ['迪迦奥特曼', '塞罗奥特曼', '银河奥特曼', '艾克斯奥特曼'] const str1 = '2022-8-16'; const arr1 = str1.split('-'); console.log(arr1); // ['2022', '8', '16']
- 3️⃣ ✔
startsWith()
- 检测是否以某个(某段)字符开头-
语法:
string.startsWith(检测字符串[, 检测位置索引号])
-
参数:
- 检测索引位置:默认 是以 0 开始
- 返回值: 布尔值(true / false)
- ⚠ 注意:
- 包前不包后
- 代码展示:
// 检测是否以某个(某段)字符开头 // str.startsWith(检测字符串[, 检测索引的位置]) // 返回值:true / false const str = '迪迦奥特曼,赛罗奥特曼,银河奥特曼,艾克斯奥特曼'; // const flag = str.startsWith('迪迦奥特曼'); // true const flag = str.startsWith('赛罗奥特曼', 6); // true console.log(flag);
-
语法:
- 4️⃣ ✔
includes()
- 判断一个字符串是否包含在另一个字符串中(返回true / false
)(includes
- 包含)-
语法:
str.includes(搜索的字符串[, 检测索引位置])
-
返回值:
true / false
- 代码展示:
// .str.includes(搜索的字符串[, 检测位置的索引]):判断一个字符串是否包含在另一个字符串中 // 返回值:true / false const str = '迪迦奥特曼,赛罗奥特曼,银河奥特曼,艾克斯奥特曼'; const flag = str.includes('赛罗奥特曼'); console.log(flag);
-
语法:
- 5️⃣
replace()
- 替换字符串中指定的字符-
语法:
字符串.replace(要替换的旧字符, 替换的新字符)
- 返回值: 替换之后 的字符串
-
注意:
- 不改变 原始字符串
- 默认 只替换 匹配到的 第一个字符串
- 可以 配合 正则表达式(g 和 i) 来 全局匹配 并且 不区分大小写
- 代码展示:
let str = '赛文奥特曼真好看,并且赛文奥特曼真厉害!'; const newStr = str.replace(/赛文/g, '迪迦'); console.log(newStr); // 迪迦奥特曼真好看,并且迪迦奥特曼真厉害!
-
语法:
5.4 Number
- Number 是 内置构造函数,用于创建数值
- toFixed(保留几位小数) - 保留小数位的长度
- 具有 四舍五入 功能
- ⚠🔻 使用该方法之后数字会转换为 字符串
const price = 12.345; const price1 = 12.921; // 没有参数 price.toFixed(); // 12 (string) price1.toFixed(); // 13 (string) // 有参数 price.toFixed(2); // 12.35 (string) price1.toFixed(2); // 12.92 (string)
六、编程思想
6.1 面向过程变成
- 面向过程: 就是分析出解决问题所需要的步骤,然后用函数把这些步骤一步一步实现,使用的时候再一个一个的依次调用就可以了
-
优点:
- 性能 比面向对象 高,适合跟硬件联系很紧密的东西,例如单片机
-
缺点:
- 没有面向对象易维护、易复用、易扩展
6.2 面向对象编程 (oop)
-
面向对象: 是把事务分解成为一个个对象,然后由对象之间分工与合作。
- 面向对象是以 功能 来划分问题的
-
特性:
- 封装性
- 继承性
- 多态性
-
优点:
- 易维护、易复用、易扩展,基于面向对象封装、继承、多态性的特性,可以设计出低耦合的系统,使系统 更加灵活、更加易于维护
-
缺点:
- 性能比面向过程低
七、构造函数
- 构造函数 体现了 面向对象 的 封装特性
- 构造函数 实例创建 的 对象 彼此独立、互不影响
- 构造函数存在的问题:浪费内存(可以用原型解决)
- 代码展示:
// 构造函数:公共的属性和方法封装到构造函数里面 function Star(uname, age, gender) { this.uname = uname; this.age = age; this.gender = gender; this.sing = function () { console.log('构造函数'); } } const dj = new Star('迪迦', 22, '男'); const sl = new Star('赛罗', 23, '女'); // 通过构造函数创建的对象彼此独立,互不影响 console.log(dj === sl); // false console.log(dj.sing === sl.sing); // false // dj 和 sl 的地址不同,所指向的function也不同
八、❗❗❗ 原型
8.1 原型
-
原型
- 是一个 对象,我们称 prototype 为 原型对象
- 跟随函数一起创建的对象
- 构造函数 通过 原型 分配的 方法 是 所有对象 所 共享的
- js规定,每一个 构造函数 都有一个 prototype 属性,指向 另一个对象,所以我们称为 原型对象
- 这个原型对象可以挂载函数,对象实例化不会多次创建原型上函数,节约内存
- 可以把那些不变的方法,直接定义在prototype对象上,这样所有对象的实例就可以共享这些方法
- 作用: 为当前函数创建的实例添加公共的属性和方法(把那些不变的方法挂载在prototype上)
- ⚠🔺 构造函数 和 原型对象 中的 this 都指向 实例对象
- 原型上的方法,谁调用就指向谁(实例对象 + 原型自己)
- 有函数 就有 原型对象
- 代码展示:
// 公共的属性写到 构造函数 里面 function Star(uname, age, gender) { this.uname = uname; this.age = age; this.gender = gender; } const Dj = new Star('迪迦', 22, '男'); const Sl = new Star('赛罗', 23, '女'); // 每个构造函数都有一个prototype(原型)属性, // console.dir(Star.prototype); // 公共的方法写在原型对象上 Star.prototype.sing = function () { console.log('原型-prototype'); } console.log(Dj.sing === Sl.sing); // true // 原型也可以调用自己身上的方法 Star.prototype.sing(); // this -> Star.prototype
// 封装一个求数组最值和和的方法(只要是数字数组就可以使用) const arr = [6, 3, 8, 2, 9, 0]; // const arr = new Array(6, 3, 8, 2, 9); Array.prototypeGgetValue = function () { return [Math.max(...this), Math.min(...this), this.reduce((prev, item) => prev + item)] } console.log(arr.GetValue()); // [9, 0, 28]
8.2 constructor属性
- 每个 原型对象 里面都有 constructor 属性(constructor ➡ 构造函数)
-
作用: 该属性 指向 该原型对象 的 构造函数
- 简单理解:指向我的爸爸,我是个有爸爸的孩子
- ⚠🔺 每个 构造函数 都有 prototype属性 ,每个 prototype属性 都有 constructor属性
-
使用场景:
- 如果有 多个对象 的 方法,我们可以 给 原型对象 采用 对象形式 赋值 (单个方法是追加)
- 但是这样就会 覆盖 构造函数 原型对象 原来 的 内容,这样 修改后 的 原型对象 的constructor属性 就 不再 指向 当前 构造函数
- 此时,我们可以在 修改后 的 原型对象 中,添加一个 constructor属性 指向 原来的构造函数
- 代码展示:
- 原来的prototype:
8.3 对象原型
-
对象 都会有一个
__proto__
指向 构造函数 的 prototype 原型对象,之所以我们对象可以使用构造函数 prototype 原型对象的属性和方法,就是因为对象有__proto__
原型的存在- ⚠ 注意:
-
__proto__
是一个 🔺只读属性🔺 (前后两个杠) - Chrome 中 [[prototype]] 和
__proto__
意义相同 - 用来表示当前实例对象指向哪个原型对象prototype
-
__proto__
对象原型 里面也有一个 constructor属性,指向 创建 该实例对象 的 构造函数
-
- ⚠ 注意:
- ⚠🔺🔺 注意: (混淆点)
- 每个 构造函数 都有 prototype(原型对象)
- 每个 prototype(原型对象) 都有 constructor,指向 该原型对象 的 构造函数
- 每个 对象 都有
__proto__
(对象原型)(属性),指向 构造函数 的 原型对象 - 每个 对象原型 都有
constructor
属性,指向 创建 该实例 的 构造函数
- 对象都会有一个属性
__proto__
指向构造函数的prototype原型对象,之所以对象可以使用构造函数prototype原型对象的属性和方法,就是因为对象有___proto__
- 🔺🔺⚠⚠总结:
- 1️⃣ prototype 是什么?哪里来的
- 原型(原型对象)
- 构造函数 都 自动有 原型
- 2️⃣ constructor属性 在哪里?作用是啥?
-
prototype原型 和 对象原型
__proto__
里面 都有 - 都 ➡ 创建 原型 / 实例对象 的 构造函数
-
prototype原型 和 对象原型
- 3️⃣
__proto__
属性在哪里?指向谁?- 在 实例对象 里面
- ➡ 原型对象
prototype
- 1️⃣ prototype 是什么?哪里来的
8.4 原型继承
- JS中大多是 借助 原型对象 实现 继承的特性
- 父构造函数(父类)
- 子构造函数(子类)
- 子类的原型 = new 父类
- 使用构造函数实现继承:
- 类:创建一类对象的模板
- 原型链继承:利用原型链的关系,实现继承的效果
- 在ES6之前,没有继承的语法,可以使用call和原型链来完成继承的效果,子构造函数继承父构造函数
- 1、子构造函数 创建的实例 拥有 父构造函数 创建的实例 相同 属性结构(call借用父构造函数的代码,修改其中this指向为子构造函数的实例)
- 2、子构造函数 创建的实例 可以调用 父构造函数 原型对象 的 公共成员(把 子构造函数 的原型对象 的 原型对象 变成,父构造函数的原型对象)
8.5 ❗❗ 原型链
- 基于 原型对象 的 继承 使得 不同构造函数 的 原型对象 联系在一起,并且这种 关联关系 是一种 链状结构,我们将 原型对象 的 链状结构关系 称为 原型链
- 从实例出发,以原型对象为连接,形成的一个链式的结构
- 由原型对象的关系相连,形成的链式结构
- 实例成员调用的时候,优先调用自身的成员,如果自身没有,继续调用原型对象的成员,如果原型对象也没有,就调用原型对象的原型对象的成员
- 原型链的作用: 实例调用成员的查找顺序
- 原型链主要指的是
__proto__
- 🔺🔺⚠⚠总结:
- 只要是 对象 就有
__proto__
,指向 原型对象 - 只要是 原型对象 里面就有 constructor,指向 创建 该原型对象 的 构造函数
- 实例对象 和 原型对象本身 可以 访问 定义在原型身上的方法或属性
- 构造函数不行(中间隔着prototype)
- 构造函数访问不了原型身上的属性或方法,因为中间隔着 prototype
- 实例对象可以访问原型对象的原型对象身上的属性和方法,中间虽然隔着东西,但能直接访问,因为这是规则
- 只要是 对象 就有
- 原型链-查找规则
- 当访问一个对象的属性(包括方法)时,首先查找这个对象自身有没有该属性
- 如果没有就查找它的原型(也就是
__proto__
指向的prototype原型对象) - 如果还没有就查找原型对象的原型(Object的原型对象)
- 依此类推一直找到Object为止(如果
Object.prototype
也没有得到的就是 ) -
__proto__
对象原型的意义在于为对象成员查找机制提供一个方向,或者说一条路线 - 可以使用
instanceof
运算符用于检测构造函数的prototype
属性是否出现在某个实例对象的原型链上
8.6 instanceof
-
instanceof
运算符用于检测构造函数的prototype
属性是否出现在某个实例对象的原型链上。- 简单理解:
- 判断 当前构造函数的原型对象 是否出现在 实例的原型链上
- 检测 实例对象 是否 由 该构造函数 创建 出来的
- 简单理解:
-
描述:
-
instanceof
运算符用来检测constructor.prototype
是否存在于参数object
的原型链上
-
-
语法:
object instanceof constructor // object 属于 constructor 吗? // 实例对象 instanceof 构造函数
-
参数:
-
object
- 实例对象 -
constructor
- 构造函数
-
- 代码展示:
// 定义构造函数 function C(){} function D(){} var o = new C(); o instanceof C; // true,因为 Object.getPrototypeOf(o) === C.prototype o instanceof D; // false,因为 D.prototype 不在 o 的原型链上 o instanceof Object; // true,因为 Object.prototype.isPrototypeOf(o) 返回 true C.prototype instanceof Object // true,同上 C.prototype = {}; var o2 = new C(); o2 instanceof C; // true o instanceof C; // false,C.prototype 指向了一个空对象,这个空对象不在 o 的原型链上。 D.prototype = new C(); // 继承 var o3 = new D(); o3 instanceof D; // true o3 instanceof C; // true 因为 C.prototype 现在在 o3 的原型链上
肆、❗❗ JavaScript高级
一、深浅拷贝
- ❗❗ 面试重点
- 开发中我们经常需要复制一个对象。如果直接用赋值会有下面问题:
- 变量之间相互赋值的时候,传递的是栈中的值
- 复杂数据类型 栈中存放的是数据地址,真正的数据存放在堆里面
- 浅拷贝 和 深拷贝 只针对 引用类型
1.1 浅拷贝
- 浅拷贝: 拷贝的是 地址
-
常见方法:
-
拷贝对象:
- 1️⃣
Object.assgin()
或者{...obj}
- 2️⃣ 使用
for - in
遍历原对象,逐个拷贝属性和属性值
- 1️⃣
-
拷贝数组:
-
Array.prototype.concat()
或者[...arr]
- 遍历原数组,逐个拷贝元素
-
-
拷贝对象:
-
浅拷贝存在的问题:
- 只能拷贝 一层 对象或数组
- 代码展示:
// 拷贝对象 const pink = { uname: 'pink老师', age: 18 }; // 1.展开运算符 - 将pink的属性展开从新加入到这个新对象里 // const copyPink = { ...pink }; // 2.静态方法-拷贝对象 const copyPink = {}; Object.assign(copyPink, pink); console.log(copyPink); // {uname: 'pink老师', age: 18} copyPink.uname = '刘德华'; console.log(copyPink); // {uname: '刘德华', age: 18} console.log(pink); // {uname: 'pink老师', age: 18} // 拷贝数组 const arr = ['迪迦', '赛罗']; // 1.展开运算符 // const copyArr = [...arr]; // 2.合并数组的方法 const copyArr = Array.prototype.concat(arr); console.log(copyArr); // ['迪迦', '赛罗'] copyArr[0] = '迪迦奥特曼'; console.log(copyArr); // ['迪迦奥特曼', '赛罗'] console.log(arr); // ['迪迦', '赛罗'] // 问题 const obj1 = { uname: 'pink老师', age: 18, family: { baby: '小pink' } }; const obj2 = { ...obj1 }; const obj3 = {}; Object.assign(obj3, obj1); obj2.family.baby = '老pink'; console.log(obj 1, obj2, obj3); // {uname: 'pink老师', age: 18, family: { baby: '老pink' }} // {uname: 'pink老师', age: 18, family: { baby: '老pink' }} // {uname: 'pink老师', age: 18, family: { baby: '老pink' }}
- 直接赋值 和 浅拷贝 有什么区别?
- 直接赋值:只要是对象,都会相互影响,因为是将栈里面的地址赋值给变量
- 浅拷贝:如果只拷贝一层对象,不相互影响,如果出现多层对象拷贝还是会相会影响
- 浅拷贝怎么理解?
- 拷贝对象之后,里面的 属性值 是 简单数据类型 直接 拷贝值
- 如果 属性值 是 引用数据类型 则 拷贝 地址
1.2 ❗❗ 深拷贝
- 深拷贝: 拷贝的是 对象 ,不是地址
- 🔺🔺❗❗❗面试必考题
-
常用方法:
- 1️⃣ 通过 函数递归 实现深拷贝
-
递归函数介绍: 如果一个函数在 内部 可以 调用自身,那么这个函数就是递归函数
- 函数内部自己调用自己
- 递归函数的作用和循环类似
- ⚠ 由于 递归 很容易 发生 “栈溢出” 错误,所以 必须加 退出条件 return
-
递归嵌套太深出现以下错误 Uncaught RangeError: Maximum call stack size exceeded 超出最大调用堆栈大小
- 代码展示:
// 利用递归函数实现 setTimeout 模拟 setInterval效果 function fn() { console.log(new Date()); setTimeout(fn, 1000); } fn();
- 实现深拷贝:
const oldObj = { uname: '迪迦', age: 22, hobby: ['打怪兽', '晒日光浴', '揍银河'], family: { wife: '卡尔蜜拉', baby: '小迪迦' } }; const newObj = {}; // 使用递归函数实现深拷贝 function deepCopy(newObj, oldObj) { // 1.遍历对象 for (let k in oldObj) { // 1.处理 属性值 = 数组 if (oldObj[k] instanceof Array) { newObj[k] = []; deepCopy(newObj[k], oldObj[k]); } // 2.处理 属性值 = 对象 else if (oldObj[k] instanceof Object) { newObj[k] = {}; deepCopy(newObj[k], oldObj[k]); } // 3.对于简单数据类型直接进行赋值操作 // 新对象.旧对象属性名 = 旧对象属性值 else { newObj[k] = oldObj[k]; } } } deepCopy(newObj, oldObj); newObj.family.twoWife = '丽娜'; console.log('旧对象:', oldObj); console.log('新对象:', newObj); // 旧对象: // { // uname: '迪迦', // age: 22, // hobby: ['打怪兽', '晒日光浴', '揍银河'], // family: {wife: '卡尔蜜拉', baby: '小迪迦'} // } // 新对象: // { // uname: '迪迦', // age: 22, // hobby: ['打怪兽', '晒日光浴', '揍银河'], // family: {wife: '卡尔蜜拉', baby: '小迪迦', twoWife: '丽娜'} // }
- ❗❗ 面试总结: 用递归函数实现过深拷贝吗?
- 1️⃣ 深拷贝:新对象不会影响旧对象
- 2️⃣ 实现深拷贝需要用到函数递归
- 3️⃣ 普通拷贝直接进行赋值操作
- 如果遇到数组,再次调用递归函数解决数组
- 如果遇到函数,直接进行赋值操作(函数就是为了实现代码的复用)
- 如果遇到对象,再次调用递归函数解决对象
- ⚠ 先解决数组或函数 再解决对象(数组和函数也属于对象)
-
递归函数介绍: 如果一个函数在 内部 可以 调用自身,那么这个函数就是递归函数
- 2️⃣ lodash里面的
cloneDeep
(JS库)- 使用之前要先引入
lodash.js
- 语法:
const newArr / newObj = _.cloneDeep(oldArr / oldObj)
- 返回值:拷贝完的对象
- 使用之前要先引入
- 3️⃣ 通过
JSON.stringify()
实现-
语法:
JSON.parse(JSON.stringify(Obj))
- 原理:
- 1️⃣ 将 对象 转换成 JSON字符串 ,基本数据类型
- 2️⃣ 然后再转换为复杂数据类型,再从堆里面开辟一个新的空间,前后两个对象没有任何关系
- ⚠ 缺点: (面试)
- 并不能转换所有的值
- 不识别 undefined 和 函数
-
语法:
- 1️⃣ 通过 函数递归 实现深拷贝
二、异常处理
- 提升代码的健壮性
2.1 throw抛异常
- 基本语法:
throw new Error('要抛出的信息')
-
总结:
- ⚠
throw
抛出异常信息,程序也会 终止执行 -
throw
后面跟的是错误提示信息 -
Error
对象配合throw
使用,能够设置更详细的错误信息
- ⚠
2.2 try / catch 捕获异常
- 捕获错误信息(浏览器提供的)
- try - 尝试(可能发生错误的代码)
- catch - 拦住
- finally - 最后(不管程序是否正确,一定会执行的里面的代码)
- ⚠ 不会自动中断程序(想要中断需要加return)
-
总结:
-
try...catch
用于捕获错误信息 - 将预估可能发生错误的代码写在
try
代码段中 - 如果
try
代码段中出现错误后,会执行catch
代码段,并截取到错误信息 -
finally
不管是否有错误,都会执行
-
- 代码展示:
// 不一定要写在函数里面 function fn() { try { const body = document.body; body.style.backgroundColor = 'purple'; } catch(err) { // 拦截错误,提示浏览器提供的额错误信息,但是不中断程序 // message:必须写(属性:错误的详细信息),err是自定义的 // console.log(err.message); // 要想中断程序必须加return // return // 还不如直接写throw throw new Error('选择器错误'); } finally { alert('不管程序是否正确,一定会执行的代码'); } } fn();
2.3 debugger
- 类似与打断点
- 调式的时候使用
- 使用:直接添加到代码对应位置
三、处理this
3.1 this指向
-
普通函数this
- 谁调用指向谁
- 🔺 独立的普通函数(不归属于任何对象)
this
指向window
- 代码展示:
let obj = { name: '阿茶', hi: function() { console.log(this); // this -> obj let fun = () => { console.log(this); // this -> obj let fn = () => { console.log(this); // this -> obj let count = function () { console.log(this); // this -> window let amount = () => { console.log(this); // this -> window } amount(); } count(); } fn(); } fun(); } }; obj.hi();
- 代码展示:
- 普通函数 没有 明确调用者 时
this
指向window
- ❌ 严格模式下没有调用者时
this
指向undefined
,严格模式写在当前作用域或全局作用域的最前面 ('user strict'
)
-
箭头函数的this
- 箭头函数中的this与普通函数完全不同,也不受调用方式的影响,箭头函数不存在this
- 箭头函数自己不会创建this,它只会沿用本体所在作用域的上层作用域的this
-
不适用: 构造函数,原型函数,dom事件函数
定义: this 是一个使用在 作用域 内部的 关键字(包括函数作用域 和 全局作用域)
3.2 ❗❗ 改变this
- 1️⃣ call()
- 使用
call
方法调用函数,同时 改变 被调用函数 中this
的值 -
语法:
函数名.call(thisArg, arg1, arg2, ...)
-
参数:
- thisArg:在函数运行时指定
this
值 - arg1, arg2:函数的实参
- ⚠ 注意:如果写入一个简单数据类型替换this,会自动使用 基本包装数据类型 把 简单数据 转换为 复杂数据类型
- thisArg:在函数运行时指定
- 返回值: 函数的返回值,因为它就是在调用函数
-
特点:
- 会 立即调用 函数(不适合用作 定时器处理函数 和 事件处理函数 )
-
作用:
- 调用函数
- 改变 this 指向
- 应用场景: 伪数组借用数组方法
- 代码展示:⬇
- 使用
- 2️⃣ ✔ apply()
-
语法:
函数名.apply(thisArg[, argsArray]);
-
参数:
-
thisarg
:在函数运行时指定的this的值(this指向谁,不改变指向使用null
) -
argsArray
:函数的实参,必须包含在 数组 里面
-
- 返回值: 函数的返回值,因为它就是在调用函数
-
特点:
- 会 立即调用 函数(不适合用作 定时器处理函数 和 事件处理函数 )
- 作用: 可以以数组的形式给某些 功能函数 传参
- 使用场景: 求数组的最值(1.for循环遍历 2.展开运算符 3.apply)
- 代码展示:⬇
const obj = {uname: '迪迦', age: 22}; function fun(x, y) { console.log(this); console.log(x + y); } fun.apply(obj, [3, 4]); // obj, 7 const max = Math.max.apply(Math, [34, 65, 12, 30]); // 65 const min = Math.min.apply(Math, [34, 65, 12, 30]); // 12
-
call 和 apply 的区别:
- 都是调用函数,都能改变
this
指向 - 参数不一样
-
call
传递的是多个值 -
apply
是以数组的形式给函数传递参数
-
- 都是调用函数,都能改变
-
语法:
- 3️⃣ ✔🔺bind()
-
bind()
方法 不会调用函数 ,但是能改变函数内部this
指向 -
语法:
函数名.bind(thisArg, arg1, arg2, ...)
-
参数:
- thisArg:this指向谁
- arg1, arg2, …:给函数传递参数
-
返回值:
- 由指定的
this
值和初始化参数改造的 原函数拷贝(新函数) - 会返回一个新的函数,一个已经改变好
this
指向的函数
- 由指定的
- 特点: 不会调用函数
- 只想改变
this
指向,并不想立即调用函数的时候,可以使用bind
(定时器内部的this
,注册事件) - 代码展示:⬇
-
- 代码展示:
const obj = { uname: '迪迦', age: 22 }; function fun(x, y) { console.log(this); console.log(x + y); } // 1、call()方法: // 语法:fun.call(thisArg[, arg1[, arg2 [, ...]]]); // 参数: // 1)要指向谁 // 2)多个参数,用来给函数传递实参 // 返回值:函数的返回值(call本质上就是在调用函数) fun.call(obj, 3, 4); // obj, 7 // 2.apply()方法: // 语法:fun.apply(thisArg[, [arg1, arg2, ...]]) // 参数: // thisArg:this指向谁 // 第二个参数是 数组 ,用来给函数传递实参 // 返回值:函数的返回值(apply本质上就是在调用函数) // 使用场景:求数组的最值 (1:for循环遍历 2:展开运算符 3:apply) fun.apply(obj, [4, 5]); // obj, 9 // 最值 const arr = [23, 67, 10, 89, 20]; console.log(Math.max.apply(Math, arr)); // 89 console.log(Math.min.apply(Math, arr)); // 10 // 3.bind()方法: // 语法:fun.bind(thisArg, arg1, arg2, ...) // 参数: // thisArg:this指向谁 // arg1, arg2:用来给函数传递参数 // 返回值:一个已经改变好this指向的函数(和原函数一样,除了this指向) // 特点:不立即调用函数 const fn = fun.bind(obj, 4, 6); console.log(fn); fn(); // bind应用 const btn = document.querySelector('button'); btn.addEventListener('click', function () { this.disabled = true; setTimeout(function () { this.disabled = false; }.bind(this), 2000); });
3.3 小结
-
相同点:
- 都可以改变函数内部的
this
指向
- 都可以改变函数内部的
-
区别点:
-
call
和apply
会 立即调用函数,并且改变函数内部的this
指向 -
call
和apply
传递的 参数不一样,call
传递参数arg1, arg2, ...
形式,apply
必须数组 形式[arg]
- ⚠
bind
不调用函数,可以改变函数内部的this
指向
-
-
主要应用场景:
-
call
调用函数 并且可以传递参数 -
apply
经常跟 数组 有关,比如借助于数学对象实现数组最值 - ⚠
bind
不调用函数,但是还想改变this
指向,比如改变 定时器内部的this
指向
-
四、性能优化
4.1 防抖
-
防抖(debounce)
- 事件发生一段时间再执行,如果这段时间内继续触发新的事件,那么取消之前的事件,只执行最新的事件;
- 延迟执行;
-
使用场景:
- 搜索框(定时器)
- 代码展示:
<input type="text"> <script> const input = document.querySelector('input'); let timerId = null; input.addEventListener('input', () => { // 先清除之前的定时器 clearTimeout(timerId); // 在开启当前的定时器 timerId = setTimeout(() => { console.log(11); }, 500); }); </script>
4.2 节流
-
节流(throttle):
- 一段时间内只执行一次事件,执行结束后才能继续执行新的事件。(限制任务执行频率的一种手段);
- 节流是一种手段,来限制任务执行频率,规则是当前任务执行结束之前,不接受新任务,直到当前任务结束,才开始执行下一个任务;
-
使用场景:
- 轮播图点击效果、鼠标移动、页面尺寸缩放(resize)、滚动条滚动
-
实现节流:
- 起始时间 - 当前时间
- 代码展示:
<style> * { margin: 0; padding: 0; } .box { width: 100px; height: 100px; background-color: red; margin-top: 30px; transition: all 1s linear; } </style> <body> <button>按钮</button> <div class="box"></div> <script> const btn = document.querySelector('button'); const box = btn.nextElementSibling; // 声明变量:作为box的初始宽度 let width = 100; // 声明变量:节流阀 let flag = true; // 事件侦听 btn.addEventListener('click', () => { if (flag) { flag = false; width += 100; box.style.width = width + 'px'; } }); // 拓展:transitionend - CSS过渡完触发事件 box.addEventListener('transitionend', () => { flag = true; }); </script> </body>
- ❗❗❗ 面试 节流 和 防抖 的区别?
-
节流: 一段时间内只执行一次事件,执行结束后才能继续执行新的事件。(限制任务执行频率的一种手段);
- 是一种手段,用来限制任务的执行频率,规则是当前任务结束之前,不会接收新任务,直到当前任务结束,才会执行新任务
- 采用两个时间(事件开始执行时间 - 事件执行结束时间)相减的方式实现,如果相减结果大与指定时间就调用函数
-
防抖: 事件发生一段时间再执行,如果这段时间内继续触发新的事件,那么取消之前的事件,只执行最新的事件;
- 定时器方式实现:在事件触发过程中一直清除定时器,当事件在一定时间内不触发就调用函数
- 使用场景:
- 节流:轮播图点击切换按钮、鼠标移动、页面尺寸缩放、滚动条滚动
- 防抖:搜索框
-
节流: 一段时间内只执行一次事件,执行结束后才能继续执行新的事件。(限制任务执行频率的一种手段);
-
案例拓展:
-
timeupdate
事件在 视频 / 音频 当前的播放位置 发生改变 时触发 -
loadeddata
事件在 当前帧的数据加载完成 且还 没有足够的数据 播放视频 / 音频的下一帧 时触发 -
currentTime
可读写 属性:获取 当前 视频 / 音频 的 时间 -
transitionend
事件在CSS完成过渡后触发
-
到了这里,关于JavaScript - 进阶+高级(笔记)的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!