作用域
-
作用域规定了变量能够被访问的"范围",离开这个范围变量便不能被访问。作用域分为;
-
局部作用域
- 函数作用域
- 在函数内部声明的变量只能在函数内部被访问,外部无法直接访问。
- 函数的参数也是函数内部的局部变量。
- 不同函数内部声明的变量无法互相访问。
- 函数执行完毕后,函数内部的变量实际被清空了。
- 块作用域
- 在 js 中使用
{ }
包裹的代码都称之为代码块。 -
let
声明的变量会产生块作用域,var
不会产生块作用域。 -
const
声明的常量也会产生块作用域。 - 不用代码块之间的变量无法互相访问。
- 推荐使用
let
和const
。
- 在 js 中使用
- 函数作用域
-
全局作用域
-
<script>
标签和.js 文件的"最外层"就是所谓的全局作用域,在此声明的变量在函数内部也可以被访问。 - 全局作用域中声明的变量,任何其他作用域都可以被访问。
- 尽量少声明全局变量,防止全局变量被污染。
-
-
作用域链
- 作用域链本质上是,底层的变量查找机制
- 在函数被执行时,会优先查找当前函数作用域中的变量
- 如果当前作用域查找不到则会依次、逐级查找父级作用域,直到全局作用域
总结:- 嵌套关系的作用域串联起来就形成了作用域链。
- 相同作用域链中按照从小到大(或叫就近原则)的规则查找变量。
- 子作用域能够访问父作用域,但是父作用域无法访问子作用域。
垃圾回收机制(Garbage Collection) 简称 GC
-
JS 中内存的分配和回收都是自动完成的,内存在不使用的时候会被垃圾回收器自动回收
- 内存的生命周期
js 环境中分配的内存,一般由如下声明周期
内存分配:当我们声明变量、函数、对象的时候,系统会自动为他们分配内存。
内存使用:即读写内存,也就是使用变量、函数等。
-
内存回收:使用完毕,由垃圾回收器自动回收不再使用的内存。
总结:- 全局变量一般不会回收。
- 一般情况下局部变量的值,不用了就会被自动回收掉。
- 内存泄漏:内存泄漏是指程序中分配的内存由于某种原因,未释放或者无法释放
常见的内存泄漏情况:
全局变量:
将本应在函数内部作用域中创建的对象或者变量不慎声明为全局变量,使得该对象在整个应用程序生命周期内始终有效,即使原本设计上只在某个特定时刻使用。未清理的引用:
定时器/回调:如果设置了定时器或者事件监听器,并且定时器回调函数或事件处理函数中持有了对大对象的引用,即使这些对象在页面其他地方已经不需要了,它们也无法被回收,因为定时器回调还在间接引用。闭包:当一个外部函数能够访问并保持对内部函数作用域中的变量引用时,即便外部函数执行完毕,那些变量也不会被回收,除非闭包内的引用被断开。
DOM 引用:
删除了 DOM 元素,但仍有 JavaScript 对象引用了该元素,这会导致 DOM 元素及其关联的数据结构不能被回收。废弃资源未释放:
例如网络请求完成后不取消请求,或者手动分配的系统资源如 Web Workers 等,在使用完毕后没有明确关闭或解除引用。
-
垃圾回收机制 - 算法说明
- 栈:由操作系统自动分配脂肪函数的参数值、局部变量等,基本数据类型放在栈里。
- 堆:一般由程序员分配释放,若程序员不释放,由垃圾回收机制回收,复杂数据类型放在堆里。
算法 1:引用计数法
IE 采用的引用计数算法,定义"内存不再使用",就是看一个对象是否有指向它的引用,没有引用了就回收对象
算法说明:跟踪记录被引用的次数,如果内引用了,就记录次数为 1,多次引用就累加,减少就-1,如果引用为 0,就释放内存。算法 2:标记清除法
现代浏览器已经不再使用引用技术法了,大多是基于标记清除法的某些改进算法,总体思想是一致的.
算法说明:从根部(全局对象)出发,定时扫描内存中的对象,能到达的对象就是还需要使用的,无法到达的对象被标记为不再使用,稍后进行回收。
- 内存的生命周期
闭包(Closure)
简单理解:闭包就是嵌套的两个函数中,内层函数 + 外层函数的变量,两个加在一起构成闭包
-
作用:封闭数据,提供操作,外部也可以访问函数内部的变量
-
常见使用场景(外部可以访问函数内部的变量)
function fn1() { let a = 10; function fn2() { console.log(a); } return fn2; } const res = fn1(); res();
-
闭包应用(实现数据的私有,统计函数调用次数)
function count() { let i = 0; function setCount() { i++; console.log(`函数被调用了${i}次`); } return setCount; } const result = count(); result(); result();
-
变量提升
只在 var 声明变量中出现
-
代码在执行之前,先去检测当前作用域下所有 var 声明的变量,把当前作用域下所有 var 声明的变量提升到当前作用域的最前面
注意点:- 只提升声明操作
(var num)
,不提升赋值操作(num=10)
。 - 变量提升只存在相同作用域中。
- let 和 const 不存在变量提升。
-
变量提升的流程:
- 先把 var 变量提升到当前作用域的最前面。
- 只提升变量声明,不提升变量赋值。
- 然后依次执行代码。
console.log(jian); var jian = 10; // 上面的代码变量提升后就是下面这段代码 var jian; console.log(jian); jian = 10;
- 只提升声明操作
函数提升
- 函数提升与变量提升类似,指的是函数在声明之前就可以被调用
注意点:
- 会把所有函数声明提升到当前作用域的最前面。
- 只提升函数声明,不是升函数调用。
- 函数表达式不存在提升的现象
// 函数表达式的例子 var fn = function () { console.log("我是函数表达式"); };
- 函数提升出现在相同作用域中。
函数参数
-
动态参数(arguments)
-
arguments
只存在于函数里面 -
arguments
是一个伪数组
function getSum() { let sum = 0; for (let index = 0; index < arguments.length; index++) { sum += arguments[index]; } return sum; } console.log(getSum(1, 2, 5, 1));
-
-
剩余参数
- 剩余参数允许将一个不定数量的参数表示为一个数组
- 剩余参数是一个真数组
function getSum(...args) { console.log(args); }
剩余参数和展开运算符的写法是一样的,写在函数的参数里是剩余参数,展开运算符是用于展开数组
箭头函数
注意:
-
箭头函数属于表达式函数,因此不存在函数提升
const fn = (x) => { return x; };
在操作 dom 时,还是建议使用普通函数
-
箭头函数的
this
- 箭头函数不会创建自己的 this,它只会从自己作用域链的上一层沿用
this
- 箭头函数不会创建自己的 this,它只会从自己作用域链的上一层沿用
-
普通函数的
this
- 谁调用就指向谁
解构赋值
- 数组赋值是将数组中的值,快速批量的赋值给一些变量的语法
- 对象解构是将对象属性和方法快速批量赋值给一系列变量的简洁语法
注意点
-
变量的名称和对象属性的名称需要保持一致
const { age } = { age: 18 };
-
对象解构的变量名改名方法
const { age: newAge } = { age: 18 }; let arr = [1, 5, 10]; let [mix, mid, max] = arr; console.log(mix, mid, max); let user = { name: "张三", age: 18, }; let { name, age, from } = user; console.log(name, age, from); const arrobj = [ { gn: "mi", gp: 1000, }, ]; const [{ gn, gp }] = arrobj; console.log(gn, gp);
forEach
const arr = [{ name: "curry" }, { name: "kobe" }, { name: "james" }];
arr.forEach((item, index) => {
console.log(item, index);
});
构造函数
是一种特殊的函数,主要用来初始化对象
-
可以快速创建多个类似的对象,把公共的属性抽取出来,封装到函数里
注意点:
- 使用 new 关键字调用函数的行为被称为实例化
- 构造函数内部无需写 return,返回值就是新创建的对象
- 实例化执行过程
- 创建新的空对象。
- 构造函数 this 指向新对象。
- 执行构造函数代码,修改 this,添加新属性。
- 返回新对象。
function Animal(name, age) { this.name = name; this.age = age; } const p1 = new Animal("悟空", 500); console.log(p1); const p2 = new Animal("八戒", 100); console.log(p2);
Object.assign()
- 用于对象的合并,将源对象(source)的所有可枚举属性,复制到目标对象(target)。
const o = { name: "curry" }; Object.assign(o, { age: 10 }); console.log(o); // {name: 'curry', age: 10} // Object.assign方法的第一个参数是目标对象(要拷贝到哪里),后面的参数都是源对象(拷贝那个对象)。
数组的方法
const arr = [
{ name: "草莓", price: 3120 },
{ name: "苹果", price: 1000 },
{ name: "苹果汁", price: 99 },
{ name: "香蕉", price: 200 },
];
// reduce 累加
const total = arr.reduce((pre, item) => {
return pre + item.price;
}, 0);
console.log(total);
// find 找到第一个符合条件的数据
const findItem = arr.find((item) => {
return item.name === "苹果";
});
console.log(findItem);
// every 检查每一项是否都符合条件,全都符合返回 true,反之返回 false
const isTrue = arr.every((item) => {
return item.price > 10;
});
console.log(isTrue);
// some 检查是否有一项符合条件,有一项符合就返回 true
const isTrue = arr.some((item) => {
return item.price > 3110;
});
console.log(isTrue);
字符串的方法
// substring 字符串截取
let str = "abCDefghijklmnopqrstuvwxyz";
let str1 = "abcdefghijklmnopqrstuvwxyz";
let str2 = "eaabbccde";
console.log("给你一张过去的" + str.substring(2, 4));
// startsWith 判断是否以某个字符串开头
console.log(str.startsWith("abc"));
console.log(str1.startsWith("abc"));
console.log(str2.startsWith("e"));
// includes 检查字符串里是否包含该
console.log(str.includes("no"));
console.log(str1.includes("no"));
console.log(str2.includes("no"));
const gift = "50g 茶叶,清洗球";
const arr = gift.split(",");
const list = arr
.map((item) => {
return `<p>[赠品]${item}</p>`;
})
.join("");
document.querySelector("body").innerHTML = list;
const arr1 = [12, 334, 6];
console.log(arr1.join("-"));
面向对象
-
面向对象的特性是
封装
继承
-
多态
注:
- js 实现面向对象需要借助构造函数来实现
- 构造函数存在浪费内存的问题
原型
- 构造函数通过原型分配的函数(方法)是所有对象可以 共享的。
- js 规定每一个构造函数都有一个
prototype
属性,指向另一个对象,我们也称之为原型对象。 - 我们可以把那些不变的方法和属性,直接绑定到
prototype
上,这样所有对象就可以共享这些属性和方法。 - 构造函数和原型对象中的
this
,都指向实例化的对象。
function Star(uname, age) {
this.uname = uname;
this.age = age;
}
Star.prototype.sing = function () {
console.log("我会唱歌");
};
Star.prototype.sex = "男";
let ldh = new Star("刘德华", 18);
let zxy = new Star("张学友", 20);
console.log(ldh.sing === zxy.sing);
console.log(ldh.sex === zxy.sex);
// -----------------------------------
const arr = [1, 2, 3, 6];
function MyNumber(arr) {
if (Array.isArray(arr)) {
this.total = function () {
return arr.reduce(function (pre, cur) {
return pre + cur;
});
};
this.max = function () {
return Math.max(...arr);
};
} else {
console.log("请传入正确格式");
}
}
console.log(new MyNumber(arr).total());
console.log(new MyNumber(arr).max());
// -----------------------------------
Array.prototype.myTotal = function () {
return this.reduce(function (pre, cur) {
return pre + cur;
}, 0);
};
Array.prototype.myMax = function () {
return Math.max(...this);
};
console.log(arr.myTotal());
console.log(arr.myMax());
consttuctor(构造函数)
每个原型对象(
prototype
)里都有个consttuctor
属性该属性指向原型对象的构造函数
-
简单来说 构造函数的爸爸和这个爸爸的儿子是一个人
function Star() {} const tu = new Star(); console.log(Star.prototype.constructor === Star);
对象都会有一个属性叫
__proto
,它指向构造函数的prototype
原型对象,之所以对象可以使用构造函数prototype
原型对象的属性和方法,就是因为对象 有__proto
原型的存在
原型链
-
当访问一个对象的属性和方法时,首先查找对象自身有没有,如果没有就再往上一层的原型对象上查找,以此类推,一直找到 Object 为 null 为止
function Star() {} const ldh = new Star(); console.log(ldh.__proto__ === Star.prototype); console.log(Star.prototype.__proto__ === Object.prototype); console.log(ldh instanceof Star); console.log(ldh instanceof Object); console.log([12] instanceof Array);
-
总结
- 所有的对象里面都有proto(对象原型),指向原型对象。
- 所有的原型对象都有 constructor,指向 创造该原型对象的构造函数。
面向对象封装 modal 提示
// 封装
function Modal(title = "标题", body = "无提示内容") {
this.box = document.createElement("div");
this.box.className = "box";
this.box.innerHTML = `
<div class="modal">
<div class="title">
<span>${title}</span>
<i>×</i>
</div>
<div class="body">
${body}
</div>
</div>`;
}
Modal.prototype.show = function () {
const hasModal = document.querySelector(".modal");
hasModal && hasModal.remove();
document.body.append(this.box);
this.box.querySelector(".title i").onclick = () => {
this.hide();
};
};
Modal.prototype.hide = function () {
this.box.remove();
};
// 使用
document.getElementById("delete").onclick = function () {
const del = new Modal("警告", "没有权限删除");
del.show();
};
document.getElementById("login").onclick = function () {
const login = new Modal("提示", "请登录");
login.show();
};
深 / 浅拷贝
-
只针对于引用类型,才会出现深 浅拷贝
- 情景 1: 直接赋值,相当于把原本对象的地址重新给了新的对象,所以其中一个修改会影响另一个
const obj = { name: "张三", }; const o = obj; o.age = 1; console.log(obj); console.log(o);
-
浅拷贝,只拷贝地址,修改拷贝后的数据不影响之前的数据
- 简单来说,就是只拷贝对象对外面的一层,如果该对象还有嵌套对象,则仍会影响嵌套的对象(类似于直接赋值)
const obj1 = { name: "李四" };
//方法 1
const o1 = { ...obj1 };
//方法 2
const o1 = Object.assign({}, obj1);
o1.age = 18;
console.log(obj1);
console.log(o1);
- 深拷贝,拷贝的是对象,而不是地址
// 方法 1 递归
// 方法 2 lodash 库的 cloneDeep
// 方法 3 JSON.stringify()
const obj2 = {
name: "王五",
age: 100,
children: {
name: "小王",
},
likes: ["篮球", "足球"],
};
const o2 = {};
// 深拷贝函数(简易版)
function deepCoop(newObj, oldObj) {
for (let key in oldObj) {
if (oldObj[key] instanceof Array) {
newObj[key] = [];
deepCoop(newObj[key], oldObj[key]);
} else if (oldObj[key] instanceof Object) {
newObj[key] = {};
deepCoop(newObj[key], oldObj[key]);
} else {
newObj[key] = oldObj[key];
}
}
}
deepCoop(o2, obj2);
o2.age = 999;
o2.children.name = "小王 2";
console.log(obj2);
console.log(o2);
异常处理
-
throw
抛出错误 -
try
/catch
把有可能发生错误的代码放到try{}
的括号中,catch(){}
用来打印错误
function add(num1, num2) {
if (!num1 || !num2) {
console.error("参数不能为空");
throw new Error("参数不能为空");
}
return num1 + num2;
}
console.log(add(6));
this 指向
- 普通函数
- 直接打印 this,指向 window。
- 严格模式下,普通函数的 this 指向 undefined。
- 谁调用,就指向谁。
- 箭头函数
- 箭头函数默认绑定的是外层 this 的值,它的 this 和外层的 this 是一样的。
- 箭头函数中的 this,沿用的是最近作用域中的 this。
- 如果涉及到 dom 操作,尽量使用普通函数。
改变 this 指向
-
call()
fn.call(需要指向的对象,参数 1,参数 2,...)
-
apply()
fn.apply(需要指向的对象,[参数 1,参数 2,...])
call 和 apply 的作用相同,但是传参方式不同
-
bind()
bind 与 apply(call)的区别,bind 不会调用函数,只是会改变 this 指向,而 apply 会直接调用函数let info = { userid: 1, usercode: "admin", }; function fn(a, b) { console.log(this, a, b); } fn(); fn.call(info, 666, 999); fn.apply(info, [666, 999]); fn.bind(info, [666, 999]);
防抖 debounce
- 在规定时间内,频繁触发事件,只执行最后一次
- 简单来说就是,假设有一段代码执行需要三秒时间,但是我频繁的触发这个事件,每次执行就会把上一次的时间取消掉,重新从三秒计时,一直到三秒内不再触发该事件,才会把这个代码执行完(只要被打断就需要重新计时)
- 使用场景:
- 搜索框,没必要用户输入一个字母就发送一次请求,可以等用户不再输入过后的 n 秒发送请求。
- 手机号,邮箱验证输入检验时,也可以使用防抖。
手写防抖
- 核心是通过 setTimeout 来实现的
声明定时器变量。
每次鼠标移动(事件触发)的时候都要先判断是否有定时器,如果有先清除以前的定时器。
如果没有定时器,则开启定时器,存入到定时器变量里面。
定时器里面写函数调用。
-
注意点:定时器里必须要
return
一个function(){}
,因为每次触发事件的时候,都需要执行防抖函数里面的代码。const moveBox = document.querySelector(".move-box"); let i = 1; function move() { moveBox.innerHTML = i++; } function debounce(fn, wait) { let timer; return function () { if (timer) { clearTimeout(timer); } timer = setTimeout(() => { fn(); }, wait); }; } moveBox.addEventListener("mousemove", debounce(move, 100));
节流 throttle
- 在规定时间内,频繁触发事件,只执行一次
- 简单来说就是,在规定的三秒内,无论触发多少次事件,只执行一次,三秒过后可以执行下一次
- 使用场景:
- 高频事件:如鼠标移动,页面尺寸缩放,滚动条滚动等
手写节流
- 节流的核心就是利用定时器 (setTimeout)来实现
声明一个定时器变量。
当鼠标每次滑动都先判断是否有定时器了,如果有定时器则不开启新定时器。
-
如果没有定时器则开启定时器,记得存到变量里面。
3.1 定时器里面调用执行的函数。
3.2 定时器里面要把定时器清空。const moveBox = document.querySelector(".move-box"); let i = 1; function move() { moveBox.innerHTML = i++; } function throttle(fn, wait) { let timer = null; return function () { if (!timer) { timer = setTimeout(() => { fn(); timer = null; }, wait); } }; } moveBox.addEventListener("mousemove", throttle(move, 500));