前言
这篇笔记,是根据B站黑马的JS网课学习整理的,用来学习的
如果有错误,还请大佬指正
作用域
(再记)
概念:
变量能够被访问使用的 “ 范围 ”
分类:
- 局部作用域
- 全局作用域
局部作用域
局部作用域分为 函数作用域 和 块作用域
函数作用域
在函数内部的作用域
函数内部声明的变量只能在函数内部被访问,外部无法直接访问
块作用域
代码块:用 { } 引起来的范围就是一个代码块
在代码块中形成的作用域,为块级作用域
不同代码块之间的变量无法互相访问
注:
- let 和 const 声明的变量会产生块作用域
- var 不会产生块作用域
全局作用域
在<script>标签内和JS文件中,书写范围即全局作用域
全局作用域中定义的变量称为全局变量
全局作用域中声明的变量,任何其它作用域都可以被访问
作用域链
本质:底层的变量查找机制
- 作用域是可以嵌套的
- 在嵌套的作用域中,当函数访问变量,会优先在当前作用域找到变量
- 若在当前作用域未找到变量,则会到上一级作用域寻找
这样的有嵌套关系的作用域串联成 作用域链
子作用域能够访问父作用域,父级作用域无法访问子级作用域
JS垃圾回收机制(了解)
概念
JS中内存的分配和回收都是自动完成的
内存在不使用的时候会被垃圾回收器自动回收
全局变量一般不会回收(关闭页面回收)
内存泄漏
不再用到的内存,无法被回收的情况 叫做 内存泄漏
内存的生命周期
- 内存分配:声明变量、函数、对象时,系统自动分配内存
- 内存使用:即读写内存,也就是使用变量、函数等
- 内存回收:使用完毕,由垃圾回收自动回收不再使用的内存
JS垃圾回收机制算法说明(拓展)
引用计数法
- 记录一个对象的使用次数
- 如果引用次数是0 ,则释放内存
缺陷:
如果两个对象相互引用,尽管已不再使用,垃圾回收器不会进行回收
因为相互引用,引用次数不为0
会造成内存泄露
标记清除法
- 从根部(在JS中就是全局对象)出发定时扫描内存中的对象。
- 凡是能从根部到达的对象,都是还需要使用的
- 无法由根部出发触及到的对象被标记为不再使用
闭包
概念:
一个函数对周围状态的引用捆绑在一起,内层函数中访问到其外层函数的作用域
人话:内层函数 + 外层函数的变量
示例:
function out(){
let i=0 //外部函数变量
function inn(){
//内部函数
console.log(i)
}
inn()
}
out()
作用 与 应用
闭包会封闭数据,但也会提供操作,使外部也可以访问函数内部的变量
function out() {
let i = 0
function inn() {
console.log(i)
}
return inn
}
//外层函数也可以使用内部函数变量
const outFun = out()
outFun()
闭包也会实现数据的私有
对比:
//未闭包代码
let i = 0
function out() {
console.log(i);
}
如上代码,其中 i 为全局变量,极其容易被修改
如果使用闭包, i 为局部变量,无法直接修改,实现数据的私有
问题:可能引起 内存泄漏
函数
函数参数
函数参数有默认值、动态参数和剩余参数
动态参数和剩余参数用于传入实参个数不确定的情况
动态参数
arguments 是函数内部内置的伪数组变量
储存了调用函数时传入的所有实参
示例:
function test(){
let sum=0
for(let i=0;i<arguments.length;i++){
sum+=arguments[i]
}
console.log(sum)
}
//可以输入多个参数
test(1,2,3)
test(1,2,3,4,5)
剩余参数
不太好形容 . . .
function test(...arr){
//...arr就是 剩余参数
console.log(arr);
}
剩余参数将一个不定数量的参数表示为一个数组
补充(. . . 语法符号)
置于最末函数形参之前,用于获取多余的实参
获取的剩余实参,是个真数组
function test00(a, b, ...arr) {
//...arr就是 剩余参数
console.log(arr);
}
test00(1,2,3,4,5)
//在函数中,a=1 ,b=2 ,其他参数存入arr中
使用. . . 语法符号获得的数组可以将一个数组进行展开
示例:
const Arr = [1, 2, 3]
console.log(...Arr);
//效果同 console.log(1,2,3)
运用场景:
//求数组最大值/最小值
const Arr01 = [1, 2, 3, 4, 5]
console.log(Math.max(...Arr01))
console.log(Math.min(...Arr01))
//合并数组
const Arr11 = [1, 2]
const Arr12 = [3, 4]
const ARR = [...Arr11, ...Arr12]
console.log(ARR);
箭头函数
引入箭头函数的目的是更简短的函数写法,箭头函数的语法比函数表达式更简洁
更适用于需要匿名函数的地方
语法
语法一:
//语法1
//传统函数
function test01(){
console.log(111)
}
test01()
//箭头函数
test02=()=>{
console.log(111)
}
test02()
语法二:只有一个参数可以省略小括号
//语法二
//传统函数
function test11(x) {
console.log(x)
}
test11(222)
//箭头函数
test12 = x => {
console.log(x)
}
test12(222)
语法三:如果函数体只有一行代码,可以写到一行上,并且无需写 return 直接返回
//语法三
//传统函数
function test21(x, y) {
return x + y
}
console.log(test21(1, 2));
//箭头函数
test22 = (x, y) => x + y
console.log(test22(2, 3));
语法四:加括号的函数体返回对象字面量表达式
//语法四
test31 = (name) => ({ Name: name })
console.log(test31('zhao'));
参数 和 this
参数:
箭头函数没有 arguments 动态参数,但是有 剩余参数
箭头函数 this:
箭头函数不会创建自己的this
在箭头函数中会沿用作用域链上一层中的 this
解构赋值
解构赋值是一种快速为变量赋值的简洁语法
本质上仍是变量赋值
分为 数组解构 和 对象解构
数组解构
概念:
将数组的单元值 快速批量赋值 给一系列变量的简洁语法
数组解构语法示例:
//数组解构
const arr = [4, 5, 6]
const [x, y, z] = arr
console.log(x, y, z)
//更简略写法
const [i, j, k] = [7, 8, 9]
console.log(i, j, k)
常规赋值,用于对比:
//常规赋值对比
const arrRoutine = [1, 2, 3]
const a = arrRoutine[0]
const b = arrRoutine[1]
const c = arrRoutine[2]
console.log(a, b, c)
其他情况:
情况一(变量多 单元值少)
const [a1, b1, c1, d1] = [1, 2, 3]
console.log(a1, b1, c1, d1)
// 1 2 3 undefined
可以赋初值解决
const [A1, B1, C1, D1 = 4] = [1, 2, 3]
console.log(A1, B1, C1, D1)
// 1 2 3 4
情况二(变量少 单元值多)
const [a2, b2] = [1, 2, 3]
console.log(a2, b2)
// 1 2
可以使用剩余参数解决
const [a3, b3,...Arr] = [1, 2, 3,4]
console.log(a3, b3,Arr)
情况三(按需导入)
const [a4, , c4, d4] = [1, 2, 3, 4]
console.log(a4, c4, d4)
// 1 3 4
情况四(多维数组套娃)
const [a5, [b5, c5]] = [1, [2, 3]]
console.log(a5, b5, c5)
// 1 2 3
应用之一(交互变量)
let n = 10
let m = 40
;[m, n] = [n, m] // ; 必加
console.log(n, m)
// 40 10
补充(两种必加分号情况)
情况一:立即执行函数
;(function name(){
console.log(111)
})()
情况二:数组解构,数组开头
;[m, n] = [n, m]
对象解构
概念:
将对象属性和方法 快速批量赋值 给一系列变量的简洁语法
语法示例:
const user = {
name: 'zhao',
age: 19
}
const { name, age } = user //{ }里面必须同名
console.log(name, age)
//更简略写法
const { x, y } = {
x: 'zhao',
y: 19
} //{ }里面必须同名
console.log(name, age)
// zhao 19
常规赋值,用于对比:
const User = {
Name: 'biao',
age: 19
}
const Name = User.Name
const Age = User.age
console.log(Name, Age)
// biao 19
各种情况与数组解构相同
数组对象解构(又是套娃)
const [{ i, j }] = [{
i: 1,
j: 2
}]
console.log(i, j)
// 1 2
对象
创建对象三种方式:
- 利用对象字面量创建对象
- 利用 new Object 创建对象
- 利用构造函数创建对象
//利用对象字面量创建对象
const test01={
name:'zzz',
age:18
}
//利用 new Object 创建对象
const test02=new Object({
name:'zzz',
age:18
})
构造函数
(个人感觉好像C++里面的类啊~~)
构造函数是一种用来 初始化对象 的特殊函数
特点:
可以通过构造函数来 快速创建 多个类似的对象
function Person(name,age,school){
this.name=name
this.age=age
this.school=school
}
const zhao=new Person('zhao',19,'清华')
const liu=new Person('liu',20,'山农')
const gao=new Person('gao',18,'北大')
//使用构造函数,直接生成三个对象
//而常规语法,只能允许创建一个对象
注:构造函数内部无需写return
约定(一些重要,但又不重要的东西,手动狗头)
- 构造函数的命名以大写字母开头。
- 构造函数只能由 "new" 操作符来执行。
实例化
使用 new 关键字调用函数的行为被称为实例化
实例化执行过程:
const zhao=new Person('zhao',19,'山农')
1.创建新的 空对象
2. 构造函数this指向新对象
3.执行构造函数代码,因为 this指向新对象,所有会为新对象添加新的属性
4.返回新对象
实例成员 与 静态成员
实例成员:
通过构造函数创建的对象称为实例对象
实例对象中的属性和方法称为实例成员
构造函数创建的实例对象彼此独立互不影响
静态成员:
构造函数的属性和方法被称为静态成员
静态成员方法中的 this 指向构造函数本身
内置构造函数
内置构造函数与内置对象的关系
内置对象:
JavaScript中的内置对象通常指的是那些在JavaScript环境中预定义的对象,这些对象提供了很多内置的方法和属性
内置构造函数则是用于创建这些内置对象的函数
例:Array、Date、Function、RegExp等内置构造函数,它们分别用于创建数组、日期对象、函数对象和正则表达式对象
部分内置构造函数
内置构造函数 | 创建类型 |
Object | 普通对象 |
Array | 数组 |
String | 字符串 |
Number | 数值 |
补充:在JS中几乎所有的数据都基于构造函数创建
示例:
const str='sdau'
//底层运行
const str=new String('sdau')
Object 部分静态方法
Object 是内置的构造函数,用于创建普通对象
静态方法就是只有构造函数Object可以调用的
方法一:
Object.keys 静态方法获取对象中所有属性
const Person = {
name: 'zhao',
age: 18
}
const arr01 = Object.keys(Person) //返回为数组
console.log(arr01);
方法二:
Object.values 静态方法获取对象中所有属性值
const Person = {
name: 'zhao',
age: 18
}
const arr02 = Object.values(Person) //返回为数组
console.log(arr02);
方法三:
Object. assign 静态方法常用于对象拷贝
const Person = {
name: 'zhao',
age: 18
}
const test={}
Object.assign(test,Person)
//将()后面的对象拷贝给前面的对象
console.log(test);
也可以用来添加成员
Object.assign(Person,{school:'SDAU'})
console.log(Person);
其他:
至于其他的方法和属性,实属繁多,就不打算记了
一方面因为懒,一方面效率不高
零零碎碎的东西,不是听一遍,记一遍,写一遍就能记住的
所有我感觉怎么一陀全记下来,效率着实堪忧
授人以鱼不如授人以渔,挂个MDN的网址比什么都强
原型
构造函数的使用, 存在浪费内存的问题
构造函数实例创建的对象彼此独立、互不影响,即使数据相同,但仍会占用多份内存
示例,如下代码:
function Person(name,age,school){
this.name=name
this.age=age
this.school=school
this.wobulijie=function(){
console.log('wobulijie')
}
}
const test01=new Person('zhao',19,'山农')
const test02=new Person('zhao',19,'山农')
console.log(test01===test02)
//返回 false
因为 test01 与 test02 占不同的两份内存,所有返回为 false
但是二者数据相同,导致浪费内存
原型对象(原型)
- 在JS中,每一个构造函数都有一个 prototype 属性,指向一个对象
- 这个对象,被称为 原型对象(原型)
- 构造函数通过原型 设置的函数方法 是所有对象所共享的
- 所有对象的实例可以共享这些方法,节约内存
function Person(name, age, school) {
this.name = name
this.age = age
this.school = school
}
//通过原型设置方法
Person.prototype.wobulijie=function(){
console.log('wobulijie')
}
const test01 = new Person('zhao', 19, '山农')
const test02 = new Person('zhao', 19, '山农')
console.log(test01.wobulijie===test02.wobulijie)
//返回为 true
综上,原型作用:
把不变的方法,直接定义在 prototype 对象上,从而实现 共享方法
注:构造函数和原型对象中的this 都指向 实例化的对象
constructor 属性
原型对象里面都有个constructor 属性,指向该原型对象的构造函数
function Person(name, age, school) {
this.name = name
this.age = age
this.school = school
}
console.log(Person.prototype.constructor)
console.log(Person)
对象原型
顾名思义,对象指向的原型(对象指向的对象原型)
对象都会有一个属性 __proto__,指向构造函数的 prototype 原型对象
注:
- __proto__ 是JS非标准属性
- [[prototype]]和__proto__意义相同
- __proto__对象原型里面也有一个 constructor属性,指向创建该实例对象的构造函数
关系图:
原型继承
可以通过构造函数来实现JS中的原型继承
方法:
将要继承的属性赋值给原型对象,使可以共享这些属性和方法
让constructor指回基础属性对象的构造函数
//要继承的 属性与方法
//被继承者
function School(){
this.schoolName='SDAU'
this.schoolAddress='泰安'
this.work=()=>{
console.log('study')
}
}
//继承者
function Student(ID,name){
this.ID=ID
this.name=name
}
//继承过程
Student.prototype=new School()
Student.prototype.contructor=Student
const zhao=new Student('123','zhao')
zhao.work()
console.log(zhao)
使用构造函数来继承,可以在给对象(继承者)添加新属性时,不影响其他对象
//要继承的 属性与方法
//被继承者
function School(){
this.schoolName='SDAU'
this.schoolAddress='泰安'
this.work=()=>{
console.log('study')
}
}
//继承者
function Student(ID,name){
this.ID=ID
this.name=name
}
function Teacher(wages,name){
this.wages=wages
this.name=name
}
//继承过程
Student.prototype=new School()
Student.prototype.contructor=Student
Teacher.prototype=new School()
Teacher.prototype.contructor=Teacher
//给Student构造函数添加新属性
Student.prototype.love=()=>{
console.log('天天摸鱼')
}
const zhao=new Student('123','zhao')
const tea=new Teacher('10000','teach')
zhao.love()
console.log(zhao)
console.log(tea)
原型链
本质:查找规则
基于原型对象的继承使得不同构造函数的原型对象关联在一起,
并且这种关联的关系是一种链状的结构,
我们将原型对 象的链状结构关系称为原型链
查找规则:
- 当访问一个对象的属性(包括方法)时,首先查找这个对象自身有没有该属性。
- 如果没有就查找它的原型(也就是 __proto__指向的 prototype 原型对象)
- 如果还没有就查找原型对象的原型(Object的原型对象)
- 依此类推一直找到 Object 为止(null)
- __proto__对象原型的意义就在于为对象成员查找机制提供一个方向,或者说一条路线
- 可以使用 instanceof 运算符用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上
深拷贝
通过递归实现
递归函数
如果一个函数在内部可以调用其本身,那么这个函数就是递归函数
注:必须要加退出条件 return
示例,利用递归写循环:
let i=0
function test(){
console.log(i)
i++
//必须要加退出条件 return
if(i>=10){
return
}
//递归
test()
}
test()
实现深拷贝
代码示例:
//递归函数实现深拷贝
//被拷贝函数
const Person = {
name: 'zhao',
age: 18,
love: [1, 2, 3],
shool: {
sname: 'sdau',
address: '泰安'
}
}
const o = {}
//深拷贝递归函数
//传入两个对象,将 old 拷贝给 new
function deepCopy(newObj, oldObj) {
//遍历对象
for (let k in oldObj) {
//如果为数组
if (oldObj[k] instanceof Array) {
newObj[k] = []
//递归
deepCopy(newObj[k], oldObj[k])
}
//如果为对象
else if (oldObj[k] instanceof Object) {
newObj[k] = {}
//递归
deepCopy(newObj[k], oldObj[k])
}
//正常
else {
newObj[k] = oldObj[k]
}
}
}
deepCopy(o, Person)
console.log(Person)
console.log(o)
通过js库实现
Lodash 是一个一致性、模块化、高性能的 JavaScript 实用工具库(官网原话)
js库lodash里面cloneDeep内部可以实现深拷贝
const test=_.cloneDeep(Person)
通过JSON实现
const test01=JSON.parse(JSON.stringify(Person))
console.log(test01)
异常处理
异常处理是指预估代码执行过程中可能发生的错误
最大程度的避免错误的发生导致整个程序无法继续运行
(我要是能预判错误,我还写这个错误干什么,再说我也没那个能力啊)
throw 抛异常
- 终止程序
- 抛出所写提示信息
<script>
throw '抛出一个异常'
</script>
- 可配合Error 对象使用
<script>
throw new Error('抛出一个异常')
</script>
try /catch 捕获异常
- 将预估错误的代码写在 try 代码段中
- 如果 try 代码段出现错误,则执行 catch 代码段
- finally 不管是否有错误,都会执行
try {
const p = document.querySelector('.p')
p.style.color='blue'
}
catch (error){
console.log(1111)
console.log(error.message)
}
finally {
console.log(2111)
}
debugger
作用:cy 插眼
改变this指向
call()
作用:调用使用call方法的函数,同时指定被调用函数中 this 的值
本质:调用函数
语法:fun . call ( 指定this值 , 参数 )
示例:
const i=[1,2]
function fn(x){
console.log(x)
console.log(this)
}
fn.call(i,111)
//this指向了i
apply()
作用:调用使用apply方法的函数,同时指定被调用函数中 this 的值(同call)
与call区别:apply()中参数传入必须是数组
语法:fun . apply( 指定this值 , [ 参数 ] )
示例:
function fnn(x,y){
console.log(x+y)
console.log(this)
}
fnn.apply(i,[1,2])
//this指向了i
bind()
作用:能改变函数内部this 指向,但不会调用函数
语法:语法:fun . bind ( 指定this值 , 参数 )
当只改变this指向,不调用函数时使用
性能优化
防抖
一个计时为n秒的函数,若在n秒内又触发了事件,则重新计时
例子:MOBA游戏中的回城
代码:
当事件触发时,先清除定时器,再重新开启定时器
//设置防抖函数
function AntiShake(fn, t) {
//fn为事件触发时执行的函数,t为时间
let timer
//返回为一个函数,因为绑定事件需要函数
return function () {
//判断有无计时器,有则关闭
if (timer) {
clearTimeout(timer)
}
//重新开启定时器
timer = setTimeout(function () {
fn()
}, t)
}
}
案例:
<body>
<div class="box"></div>
<script>
const box = document.querySelector('.box')
let i = 1
//设置防抖函数
function AntiShake(fn, t) {
//fn为事件触发时执行的函数,t为时间
let timer
//返回为一个函数,因为绑定事件需要函数
return function () {
//判断有无计时器,有则关闭
if (timer) {
clearTimeout(timer)
}
//重新开启定时器
timer = setTimeout(function () {
fn()
}, t)
}
}
//设置事件触发时执行的函数
function fn() {
box.innerHTML = i++
}
box.addEventListener('mousemove', AntiShake(fn, 500))
</script>
</body>
节流
一个计时为n秒的函数,在n秒内无论怎么触发事件,都不影响计时
例子:技能CD
代码:
记录时间差,小于差值则不执行,大于则执行
//设置节流函数
function Throttle(fn, t) {
//fn为事件触发时执行的函数,t为时间
let timer=0
//返回为一个函数,因为绑定事件需要函数
return function(){
//记录当前时间
let now=new Date()
//时间差=当前时间-触发事件时的时间
let timeDifference=now-timer
//判断,如果时间差>t,则执行
if(timeDifference>t){
fn()
//记录触发事件时的时间
timer=now
}
}
}
案例:
<body>
<div class="box"></div>
<script>
const box = document.querySelector('.box')
let i = 1
//设置节流函数
function Throttle(fn, t) {
//fn为事件触发时执行的函数,t为时间
let timer=0
//返回为一个函数,因为绑定事件需要函数
return function(){
//记录当前时间
let now=new Date()
//时间差=当前时间-触发事件时的时间
let timeDifference=now-timer
//判断,如果时间差>t,则执行
if(timeDifference>t){
fn()
//记录触发事件时的时间
timer=now
}
}
}
//设置事件触发时执行的函数
function fn() {
box.innerHTML = i++
}
box.addEventListener('mousemove',Throttle(fn,500))
</script>
</body>