js(高阶)
作用域
规定了变量能够 被访问的“范围”,离开了这个“范围”变量便不能被访问
局部作用域
1.函数作用域
在函数内部声明的变量只能在函数内部被访问,外部无法直接访问。
<script>
function getSum(){
//函数内部是函数作用域 属于局部表量
const num = 10
}
console.log(num)
//此处报错 函数外部不能使用局部作用域变量
</script>
总结:
1.函数内部声明的变量,在函数外部无法被访问
2.函数的参数也是函数内部的局部变量
3.不同函数内部声明的变量无法互相访问
4.函数执行完毕后,函数内部的变量实际被清空了
2.块作用域
在JavaScript中使用{ }包裹的代码称为代码块,代码块内部声明的变量外部将有可能无法被访问。
for(let t = 1; t <= 6: t++){
//t 只能在该代码块中被访问
console.log(t)//正常
}
//超出了t的作用域
console.log(t)//报错
总结:
1.let声明的变量会产生块作用域,var不会产生块作用域
2.const声明的常量也会产生块作用域
3.不同代码块之间的变量无法互相访问
4.推荐使用let或const
全局作用域
<script>标签和.js文件的【最外层】就是所谓的全局作用域,再次声明的变量在函数内部也可以被访问。
全局作用域中声明的变量,任何其他作用域都可以被访问
<script>
//全局作用域
//全局作用域下声明了num变量
const num = 10
function fn(){
console.log(num)
}
//此处全局作用域
注意:
1.为window对象动态添加的属性默认也是全局的,不推荐
2.函数中未使用任何关键字声明的变量为全局变量,不推荐!!!
3.尽可能少的声明全局变量,防止全局变量被污染
作用域链
作用域链本质上是底层的变量查找机制。
1.在函数被执行时,会优先查找当前函数作用域中查找变量
2.如果当前作用域查找不到则会依次逐级查找父级作用域直到全局作用域
<script>
//全局作用域
let a = 1
let b = 2
//局部作用域
function f(){
let a = 1
//局部作用域
function g(){
a = 2
console.log(a)
}
g()//调用g
}
f()//调用f
</script>
总结:
1.嵌套关系的作用域串联起来形成了作用域链
2.相同作用域链中按着从小到大的规则查找变量
3.子作用域能够访问父作用域,父级作用域无法访问子级作用域
js垃圾回收机制(GC)
JS中内存的分配和回收都是自动完成的,内存在不使用的时候会被垃圾回收器自动回收
内存的生命周期
js环境中分配的内存,一般有如下生命周期:
1.内存分配:当我们声明变量,函数,对象的时候,系统会自动为他们分配内存
2.内存使用:即读写内存,也就是使用变量,函数等
3.内存回收:使用完毕,由垃圾回收器自动回收不再使用的内存
//为变量分配内存
const age = 18
//为对象分配内存
const obj = {
age :19
}
//为函数分配内存
function fn(){
const age = 18
console.log(age)
}
说明:
1.全局变量一般不会回收(关闭页面回收)
2.一般情况下局部变量的值,不用了,会被自动回收掉
内存泄漏:
程序中分配的内存由于某种原因程序未释放或无法释放叫做内存泄漏
js垃圾回收机制—算法说明
堆栈空间分配区别:
1.栈(操作系统):由操作系统自动分配释放函数的参数值,局部变量等,基本数据类型放到栈里面。
2.堆(操作系统):一般由程序员分配释放,若从程序员不释放,由垃圾回收机制回收。复杂数据类型放到堆里面。
引用计数法
IE采用的引用计数算法,定义“内存不再使用”,就是看一个对象是否有指向它的引用,没有引用了就回收对象
算法:
跟踪记录被引用的次数
如果被引用了一次,那么就记录次数1,多次引用会累加++
如果减少一个引用就减1 --
如果引用次数是0,则释放内存
致命的问题(嵌套引用(循环引用))
如果两个对象相互引用,尽管它们已不再使用垃圾回收器不会再进行回收,导致内存泄漏
function fn(){
let o1 = {}
let o2 = {}
o1.a = o2
o2.a = o1
return '引用计数无法回收'
}
fn()
//因为他们的引用次数永远不会是0,这样的相互引用如果说是很大量的存在就会是导致大量的内存泄漏
标记清除法
现代的浏览器已经不在使用引用技术算法了
现代浏览器通用的大多是基于标记清除算法的某些改进算法,总体思想都是一致的。
核心:
1.标记清除算法将“不再使用对象”定义为“无法达到的对象”。
2.就是从根部(在js中就是全局对象)出发定时扫描内存中华的对象。但凡能从根部达到的对象都是还需要使用的。
3.那些无法使用由根部出发触及到的对象被标记为不再使用,稍后进行回收。
闭包
概念:一个函数对周围状态的引用捆绑在一起,内层函数中访问到其外部函数的作用域
简单理解:
闭包 = 内层函数 + 外层函数的变量
function outer(){
const a = 1
function f(){
console.log(a)
}
f()
}
outer()
闭包作用:封闭数据,提供操作,外部也可以访问函数内部的变量
闭包的基本格式:
function outer(){
let i = 1
function fn(){
console.log(i)
}
return fn
}
const fun = outer()
fun()//1//调用函数
//外层函数使用内部函数的变量
闭包的应用:实现数据的私有
比如我们要做个统计函数调用次数,函数调用一次,就++
let const = 1
function fn(){
count++
console.log('函数被调用${count}次')
}
fn()
fn()
//这个const是个全局变量,很容易被修改
function fn(){
let count = 1
function fun(){
count++
console.log('函数被调用${count}次')
}
return fun
}
const result = fn()
result()
result()
//这样实现了数据私有,无法直接修改count
变量提升
是js中比较’奇怪‘的现象,它允许在变量声明之前即被访问(仅存在于var声明变量)
1.把所有var声明的变量提升到当前作用域的最前面
2.只提升声明,不提升赋值
console.log(num + '件')
var num = 10
//只有var能用这种方法
注意:
1.变量在未声明即被访问时会报语法错误
2.变量在var声明之前即被访问,变量的值为underfined
3.let/const 声明的变量不存在变量提升
4.变量提升出现在相同作用域当中
5.实际开发中推荐先声明在访问变量
函数进阶
函数提升
函数提升与变量提升比较类似,是指函数在声明之前即可被调用
1.把所有函数声明的变量提升到当前作用域的最前面
2.只提升函数声明,不提升函数调用
fn()
function fn()
{
console.log('函数提升')
}
总结:
1.函数提升能够使函数的声明调用更灵活
2.函数表达式不存在提升的现象
3.函数提升出现在相同作用域当中
函数参数
函数参数的使用细节,能够提升函数应用的灵活性
1.动态参数(argument)
是函数内部内置的伪数组变量,它包含了调用函数时传入的所有实参
function sum()
{
let s = 0
for(let i = 0;i < argument.length;i++){
s +=argument[i]
}
console.log(s)
}
//调用求和函数
sum(5,10)//两个参数
sum(5,1,2)//三个参数
总结:
1.argument是一个伪数组
2.argument的作用是动态获取函数的实参
3.可以通过for循环依次得到传递过来的实参
2.剩余参数
剩余参数允许我们将一个不定数量的参数表示为一个数组
function sum(...s)
{
//s得到了【5,10】
console.log(s)
}
//调用求和函数
sum(5,10)//两个参数
1...是语法符号,置于最末函数形参之前,用于获取多余的实参
2.借助...获取的剩余实参,是一个真数组
展开运算符
展开运算符(......),将一个数组进行展开
const arr = [1,5,3,8,2]
console.log(...arr)//1 5 3 8 2
说明:
不会修改数组
求数组的最值
const arr = [1,5,3,8,2]
console.log(Math.max(...arr))//8
箭头函数
目的:引入箭头函数的目的是更简短的函数写法并且不绑定this,箭头函数的语法比函数变大时更简洁
使用场景:
箭头函数更适用于那些本来需要匿名函数的地方
基本语法
const 函数名 = (参数)=>{
//函数体
}
//调用
函数名(参数)
更多简写形式:
1.如果箭头函数只有一个形参的时候可以省略小括号:
const 函数名 = 参数=>{ 函数体 }
2.如果只有一行代码的时候,可以省略大括号:
const 函数名 = 参数 => 函数体
3.如果只有一行代码并且有返回值,可以省略大括号和return:
const 函数名 = 参数 => 参数
箭头函数可以直接返回一个对象
const 函数名 = (参数) => ({属性名:参数})
特点:
箭头函数属于表达式函数,因此不存在函数提升
箭头函数只有一个参数时可以省略圆括号
()
箭头函数函数体只有一行代码时可以省略花括号
{}
,并自动做为返回值被返回
箭头函数参数
箭头函数中没有 arguments
,只能使用 ...
动态获取实参
//使用箭头函数的剩余参数写一个求和函数
const getSum = (...arr) =>{
let sum = 0
for(let i = 0; i<arr.length; i++){
sum += arr[i]
}
return sum
}
const re = getSum(1,2,3)
console.log(re)
箭头函数this
箭头函数不会创建自己的this,它只会从自己的作用域链的上一层沿用this。
解构赋值
数组解构
数组解构是将数组的单元值快速批量赋值给一系列变量的简洁语法。
语法:let [ ] = 数组【赋值运算符 = 左侧的 [] 用于批量声明变量,右侧数组的单元值将被赋值给左侧的变量】
变量的顺序对应数组单元值得位置依次进行赋值操作
<script>
// 普通的数组
let arr = [1, 2, 3]
// 批量声明变量 a b c
// 同时将数组单元值 1 2 3 依次赋值给变量 a b c
let [a, b, c] = arr
//第二种写法:
//let [a, b, c] = [1, 2, 3]
console.log(a); // 1
console.log(b); // 2
console.log(c); // 3
</script>
Js 前面有两哪种情况需要加分号的?
立即执行函数
数组解构(;[b,a] = [a,b])
对象解构
对象解构是将对象属性和方法快速批量赋值给一系列变量得简洁语法
基本语法
赋值运算符 = 左侧的{}用于批量声明变量,右侧对象的属性值将被赋值给左侧的变量
对象属性的值将被赋值给与属性名相同的变量
注意解构的变量名不要和外面的变量名冲突否则报错
对象中找不到与变量名一致的属性时变量值为underfined
//普通对象
const user = {
name:'小明',
age : 18
};
//批量声明变量 name age
//同时将数组单元值 小明 18 依次赋值给变量 name age
const {name ,age} = user
console.log(name)//小明
console.log(age)//18
遍历数组forEach方法(重点)
forEach方法方法用于调用数组的每个元素,并将元素传递给回调函数
只要使用场景:遍历数组的每个元素
语法:
被遍历的数组.forEach(function(当前数组元素,当前元素索引号))
例如:
const arr = ['pink','red','green']
arr.forEach(function(item,index){
console.log('当前数组元素是${item}')//依次打印数组每一个元素
console.log(当前数组yua元素的suo索引是:'${index}')
//依次打印数组每一个元素的索引
})
forEach主要是遍历数组
参数当前数组元素是必须要写的,索引号可选
深入对象
创建对象三种方式
1.利用对象字面量创建对象
const o = {
name:'佩奇'
}
2.利用new Objec创建对象
const o = new Object({name : '佩奇'})
console.log(o)
//{name:'佩奇'}
3.利用构造函数创建对象
构造函数
是一种特殊的函数,主要用来初始化对象
使用场景:
常规的{...}语法允许创建一个对象。比如我们创建了佩奇的对象,继续创建乔治的对象还需要重新写一遍,此时可以通过构造函数来快速创建多个类似的对象
function Pig(name,age,gender){
this.name = name
this. age = age
this.gener = gender
}
//创建佩奇对象
const Peppa = new Pig('佩奇',6,'nv')
const George = new Pig('qiao'z乔治',3,'男')
const Mum = new Pig('猪妈妈',30,'女')
const Dad = new Pig('猪爸爸',32,'男')
构造函数在技术上是常规函数,不过有两个约定:
它们的命名以大写字母开头。
它们只能由"new"操作符来执行
语法:
//创建构造函数
function 函数名(参数1,参数2,...){
this.属性1 = 参数1
this.属性2 = 参数2
...
}
//new关键字调用函数
const 变量名 = new 函数名(参数1,参数2,...)
特点:
使用
new
关键字调用函数的行为被称为实例化实例化构造函数时没有参数时可以省略
()
构造函数无需写
return
,返回值即为新创建的对象构造函数内部的
return
返回的值无效!
实例化执行过程
说明:
创建新的空对象
构造函数this指向新对象
执行构造函数代码,修改this,添加新的属性
返回新对象
实例成员
实例成员
通过构造函数创建的对象称为实例对象,实例对象中的属性和方法称为实例成员(实例属性和实例方法)。
简单来说就是实例成员=实例对象上的属性和方法
function Person(){
//构造函数内部的this就是shi'li'dui实例对象
//实例对象中动态添加属性
this.name = '小明'
this.sayHi = function (){
console.log('大家好~')
}
}
//实例化,p1是实例对象
//p1实际就是构造函数内部的this
const p1 = new Person()
console.log(p1)
console.log(p1.name)//访问实例调用
p1.sayHi()//调用实例方法
说明:
1.为构造函数传入参数,创建结构相同但值不同的对象
2.构造函数创建的实例对象彼此独立互不影响
3.构造函数内部 this 实际上就是实例对象,为其动态添加的属性和方法即为实例成员。
静态成员
静态成员
在 JavaScript 中底层函数本质上也是对象类型,因此允许直接为函数动态添加属性或方法,构造函数的属性和方法被称为静态成员。
function Person(){
//构造函数内部的this就是shi'li'dui实例对象
//实例对象中动态添加属性
this.name = '小明'
this.sayHi = function (){
console.log('大家好~')
}
}
//静态属性
Person.num = 'balabal'
//静态方法
。。。。
特点:
1.一般公共特征的属性或方法静态成员设置为静态成员
2.静态成员只能构造函数来访问
3.静态成员方法中的 this 指向构造函数本身
内置构造函数
在 JavaScript 中最主要的数据类型有 6 种,分别是:字符串、数值、布尔、undefined、null 和 对象
常见的对象类型数据包括数组和普通对象。
其中字符串、数值、布尔、undefined、null 也被称为简单类型或基础类型,
对象也被称为引用类型。
但是我们会发现有些特殊情况:
const str = 'andy'
console.log(str.length)//4
其实字符串,数值,布尔等基本类型也都有专门的构造函数,这些我们称为包装类型。
js中几乎所有的数据都可以基于构成函数创建
1.引用类型:Object,Array,RegExp,Date等
2.包装类型:String,Number,Boolean等
Object
Object
是内置的构造函数,用于创建普通对象。不过还是推荐使用字面量方式声明对象,而不是Object构造函数。
Object.keys 静态方法获取对象中所以属性
语法:Object.keys(),返回的是一个数组
Object.values 静态方法获取对象中所以属性值
语法:Object.values(),返回的是一个数组
Object.assign 静态方法常用语对象拷贝
语法:Object.assign(接收对象,拷贝对象),返回的是一个数组
这个方法经常适用于给对象添加新的属性:
const o ={ name: '佩奇',age: 6}
Object.assign(o,{gender:'女'})
console.log(o)
//name:'佩奇',age:6,gender:'女'
Array
是内置的构造函数,用于创建数组,不过还是建议使用字面量创建,不用Array构造函数创建。
实例方法:
forEach----遍历数组(不返回数组,经常用于查找遍历数组元素)
filter——过滤数组(返回新数组,返回的是筛选满足条件的数组元素)
map——迭代数组(返回新数组,返回的是处理之后的数组元素,想要使用返回的新数组)
reduce——累计器(返回累计处理的结果,经常用于求和等)
数组常见方法汇总:
1.实例方法 forEach 用于遍历数组,替代 for 循环 (重点🌟)
2.实例方法 filter 过滤数组单元值,生成新数组 (重点🌟)
3.实例方法 map 迭代原数组,生成新数组 (重点🌟)
4.实例方法 join 数组元素拼接为字符串,返回字符串 (重点🌟)
5.实例方法 find 查找元素, 返回符合测试条件的第一个数组元素值,如果没有符合条件的则返回 undefined (重点🌟)
6.实例方法every 检测数组所有元素是否都符合指定条件,如果所有元素都通过检测返回 true,否则返回 false (重点🌟)
7.实例方法some 检测数组中的元素是否满足指定条件 如果数组中有元素满足条件返回 true,否则返回 false
8.实例方法 concat 合并两个数组,返回生成新数组
9.实例方法 sort 对原数组单元值排序
10.实例方法 splice 删除或替换原数组单元
11.实例方法 reverse 反转数组
12.实例方法 findIndex 查找元素的索引值
String
String
是内置的构造函数,用于创建字符串。
<script>
// 使用构造函数创建字符串
let str = new String('hello world!');
// 字面量创建字符串
let str2 = '你好,世界!';
// 检测是否属于同一个构造函数
console.log(str.constructor === str2.constructor); // true
console.log(str instanceof String); // false
</script>
String汇总:
1.实例属性 length 用来获取字符串的度长(重点🌟)
2.实例方法 split('分隔符') 用来将字符串拆分成数组(重点🌟)
3.实例方法 substring(需要截取的第一个字符的索引[,结束的索引号]) 用于字符串截取(重点🌟)
4.实例方法 startsWith(检测字符串[, 检测位置索引号]) 检测是否以某字符开头(重点🌟)
5.实例方法 includes(搜索的字符串[, 检测位置索引号]) 判断一个字符串是否包含在另一个字符串中,根据情况返回 true 或 false(重点🌟)
6.实例方法 toUpperCase 用于将字母转换成大写
7.实例方法 toLowerCase 用于将就转换成小写
8.实例方法 indexOf 检测是否包含某字符
9.实例方法 endsWith 检测是否以某字符结尾
10.实例方法 replace 用于替换字符串,支持正则匹配
11.实例方法 match 用于查找字符串,支持正则匹配
Number
Number
是内置的构造函数,用于创建数值。
<script>
// 使用构造函数创建数值
let x = new Number('10')
let y = new Number(5)
// 字面量创建数值
let z = 20
</script>
常用方法:toFixed()
设置保留小数位的长度
// 数值类型
const price = 12.345
// 保留两位小数 四舍五入
console.log(price.toFixed(2)) //12.35
编程思想
1.面向过程
面向过程就是分析出解决问题所需要的步骤,然后用函数把这些**步骤一步一步实现,使用的时候再一个一个的依次。
优点:
性能比面向对象高,适合跟硬件联系很紧密的东西,例如单片机就采用的面向过程编程。
缺点:
没有面向对象易维护、易复用、易扩展
2.面向对象
面向对象是把事务分解成为一个个对象,然后由对象之间分工与合作。
在面向对象程序开发思想中,每一个对象都是功能中心,具有明确分工。
面向对象编程具有灵活、代码可复用、容易维护和开发的优点,更适合多人合作的大型软件项目。
面向对象的特性:封装性、继承性、多态性
优点:
易维护、易复用、易扩展,由于面向对象有封装、继承、多态性的特性,可以设计出低耦合的系统,使系统更加灵活、更加易于维护
缺点:
性能比面向过程低
构造函数
封装是面向对象思想中比较重要的一部分,js面向对象可以通过构造函数实现的封装。同样的将变量和函数组合到了一起并能通过this实现数据的共享,所不同的是借助构造函数创建出来的实例对象之间是彼此不影响的。
特点:
构造函数体现了面向对象的封装特性
构造函数实例创建的对象彼此独立、互不影响
js面向对象可以通过构造函数实现的封装
存在
浪费内存
的问题(因为两个对象内存地址不一样,即使是同样的属性也会开辟一个新空间)
原型
简单来说就是:一个叫做prototype对象,我们叫做原型对象。作用是共享方法,可以把那些不变的方法,直接定义在prototype对象上。
<script>
function Person() {
}
// 每个函数都有 prototype 属性
console.log(Person.prototype)
</script>
总结:
公共的属性写到构造函数里面
公共的方法写到原型对象里面
原型
1.构造函数通过原型分配的函数是所有对象所共享的
2.JavaScript 规定,每一个构造函数都有一个 prototype 属性,指向另一个对象,所以我们也称为原型对象
3.这个对象可以挂载函数,对象实例化不会多次创建原型上函数,节约内存
4.我们可以把那些不变的方法,直接定义在 prototype 对象上,这样所有对象的实例就可以共享这些方法。
5.构造函数和原型对象中的this都指向实例化的对象
构造函数的this指向:构造函数里面的this就是实例对象
原型对象的this指向:也是构造函数里面的this就是实例对象
constructor属性
每个原型对象里面都有个constructor 属性(constructor 构造函数)
作用:该属性指向该原型对象的构造函数, 简单理解,就是“指向我的爸爸,我是有爸爸的孩子”。
使用场景:
如果有多个对象的方法,我们可以给原型对象采取对象形式赋值,但是这样就会覆盖构造函数原型对象原来的内容,这样修改后的原型对象 constructor 就不再指向当前构造函数了,此时,我们可以在修改后的原型对象中,添加一个 constructor 指向原来的构造函数。
语法:
Star.prototype={
//手动利用constructor指向Star构造函数
constrcutor:Star,
//方法...
}
对象原型
构造函数可以创建实例对象。
构造函数还有一个原型对象,一些公共的属性或者方法放到这个原型对象身上。
但是为什么实例对象可以访问原型对象里面的属性和方法呢❓
对象都会有一个属性 __proto__
**指向构造函数的 prototype 原型对象,之所以我们对象可以使用构造函数 prototype 原型对象的属性和方法,就是因为对象有 __proto__
原型的存在。
注意:
proto 是JS非标准属性,有些浏览器可能是不一样的,还有一个[[prototype]]
[[prototype]]和__proto__意义相同
用来表明当前实例对象指向哪个原型对象prototype
proto对象原型里面也有一个 constructor属性,指向创建该实例对象的构造函数
原型继承
继承是面向对象编程的另一个特征,通过继承进一步提升代码封装的程度,JavaScript 中大多是借助原型对象实现继承。
子类的原型 = new 父类
原型链
基于原型对象的继承使得不同构造函数的原型对象关联在一起,并且这种关联的关系是一种链状的结构,我们将原型对象的链状结构关系称为原型链。
原型链其实就是一个查找规则:
1.当访问一个对象的属性(包括方法)时候,首先查找这个对象自身有没有该属性。
2.如果连我有就查找它的原型(也就是__proto__指向的prototype对象)
3.如果还没有就查找原型对象的原型(Object的原型对象)
依此类推一致找到Object为止(null)
proto对象原型的意义就在于为对象成员查找机制提供一个方向,可以使用instanceof运算符用于检测构造函数的prototype属性是否出现在某个实例对象的原型链上。
function Person() { }
const p = new Person()
console.log(p instanceof Person) //true
console.log(p instanceof Object) //true
console.log(p instanceof Array) //false
console.log([1, 2, 3] instanceof Array) //true
console.log(Array instanceof Object) //true 因为万物皆对象
深浅拷贝
在开发中我们经常需要复制一个对象,但是在复制对象的过程中会出现一些问题,这就涉及到了深浅拷贝的事啦。
正常赋值方法:
const obj = {
uname:'pink'
age:18
}
const o = obj
console.log(o)
//但是修改起来会发生一些情况
o.age = 20
console.log(o)//age=20
console.log(obj)/age=/20
//obj的值也被改了
首先浅拷贝和深拷贝只针对引用类型
浅拷贝:拷贝的是地址
常用方法:
1.拷贝对象:Object.assgin()或者展开运算符 {...obj} 拷贝对象
2.Array.prototype.concat()
或者 [...arr]
直接赋值和浅拷贝有什么区别?
直接复制的方法,只要是对象,都会相互影响,因为是直接拷贝对象栈里面的地址
浅拷贝如果是一层对象,不相互影响,如果出现多层对象拷贝还会相互影响。
const obj = {
uname: 'pink',
age: 18,
family: {
baby: "小pink"
}
};
// 使用Object.assign进行浅拷贝
const o = {};
Object.assign(o, obj);
o.age = 20;//单层
o.family.baby = '老pink';//多层
console.log(o);
console.log(obj);
//那obj的family被改了,age没被改
//如果是简单数据类型拷贝值,引用数据类型拷贝的是地址 (简单理解: 如果是单层对象,没问题,如果有多层就有问题)
深拷贝:拷贝的是对象
常用方法:
1.通过递归实现深拷贝
如果一个函数在内部可以调用其本身,那么这个函数就是递归函数,递归函数的作用和循环效果类似,由于递归很容易发生“栈溢出”错误,所以必须要加退出条件return
let i = 1
function fn(){
console.log('这是第${i}次打印')
if(i>=6){return}
i++
fn()
}
fn()
函数递归:利用递归函数实现setTimeout模拟setInterval效果
function getTime(){
document.querySelector('div').innerHTML = new Date().toLocaleString()
setTimeout(getTime ,1000)
}
getTime()
如何通过递归函数实现深拷贝
将一个个属性传过去,前提是遇到简单类型,若是遇到数组这种复杂类型,还得在下一层
2.lodash/cloneDeep
js库lodash里面cloneDeep内部实现了深拷贝
const o = _.cloneDeep(obj)
3.通过JSON.stringify()实现
JSON.stringify()
//把对象转换为JSON字符串
JSON.parse(JSON.stringify(obj))
//再把字符串转为对象
异常处理
throw抛异常
异常处理是指预估代码执行过程中可能发生的错误,然后最大程度的避免错误的发生导致整个程序无法继续运行。
语法:throw new Error()
总结:
throw 抛出异常信息会中断,程序也会终止执行
throw 后面跟的是错误提示信息
Error 对象配合 throw 使用,能够设置更详细的错误信息
<script>
function counter(x, y) {
if(!x || !y) {
// throw '参数不能为空!';
throw new Error('参数不能为空!')
}
return x + y
}
counter()
</script>
try/catch捕获异常
我们可以通过try
捕获错误信息(浏览器提供的错误信息)
特点:
try...catch
用于捕获错误信息将预估可能发生错误的代码写在
try
代码段中如果
try
代码段中出现错误后,会执行catch
代码段,并截获到错误信息finally
不管是否错误,都会执行
function foo() {
try {
// 查找 DOM 节点
const p = document.querySelector('.p')
p.style.color = 'red'
} catch (error) {
// try 代码段中执行有错误时,会执行 catch 代码段
// 查看错误信息
console.log(error.message)
// 终止代码继续执行
return
}
finally {
//不管你程序对不到,一定会执行的代码
alert('执行')
}
console.log('如果出现错误,我的语句不会执行')
}
foo()
debugger
相当于断点调试,可以在运行的时候直接调转过去
this
普通函数
普通函数的调用方式决定了 this 的值,即【谁调用 this 的值指向谁】。
普通函数没有明确调用者时 this 值为 window,严格模式下没有调用者时 this 的值为 undefined。
箭头函数
箭头函数中的 this 与普通函数完全不同,也不受调用方式的影响,事实上箭头函数中并不存在 this !
特点:
箭头函数会默认帮我们绑定外层this的值,所以在箭头函数中this的值和外层的this是一样的。
箭头函数中的this引用的就是最近作用域中的this
向外层作用域中,一层一层查找this,直到this的定义
注意
⚠️注意:
事件回调函数使用箭头函数时,this 为全局的 window,因此DOM事件回调函数不推荐使用箭头函数:
<script>
// DOM 节点
const btn = document.querySelector('.btn')
// 箭头函数 此时 this 指向了 window
btn.addEventListener('click', () => {
console.log(this)
})
// 普通函数 此时 this 指向了 DOM 对象
btn.addEventListener('click', function () {
console.log(this)
})
</script>
基于原型的面向对象也不推荐采用箭头函数:
<script>
function Person() {
}
// 原型对像上添加了箭头函数
Person.prototype.walk = () => {
console.log('人都要走路...')
console.log(this); // window
}
const p1 = new Person()
p1.walk()
</script>
this中适用于不适用的情况:
适用:需要使用上层this的地方
不适用:构造函数、原型函数、dom事件函数等
改变this指向
以上归纳了普通函数和箭头函数中关于 this
默认值的情形,不仅如此 JavaScript 中还允许指定函数中 this
的指向,有 3 个方法可以动态指定普通函数中 this
的指向:
1.使用 call
方法调用函数,同时指定函数中 this
的值。
语法:函数名.call(thisArg,arg1,arg2,...)
thisArg
:在fun函数运行时指定的this值arg1,arg2
:传递的其他参数返回值就是函数的返回值,因为它就是调用函数
const obj = {
uname:'pink'
}
function fn(x,y){
console.log(this)//window
console.log(x+y)
}
//1.调用函数
//2.改变this值
fn.call(obj,1,2)
2.apply
使用apply方法调用函数,同时指定被调用函数中this的值。
语法:函数名.apply(thisArg, [argsArray])
(要求第二个参数必须是数组)
thisArg
:在fun函数运行时指定的this值argsArray
:传递的值,必须包含在数组里面
注意:返回值就是函数的返回值,因为它就是调用函数,因此 apply主要跟数组有关系,比如使用 Math.max() 求数组的最大值。
const obj = {
uname:'pink'
}
function fn(x,y){
console.log(this)//window
console.log(x+y)
}
//1.调用函数
//2.改变this值
fn.call(obj,[1,2])
//返回值 本身就是在调用函数,所以返回值就是函数的返回值
call和apply的区别是?
都是调用函数,都能改变this指向
参数不一样,apply传递的必须是数组
3.bind
bind
方法并不会调用函数,而是创建一个指定了 this
值的新函数。
语法:函数名.bind(thisArg, arg1, arg2, ...)
thisArg
:在fun函数运行时指定的this值arg1,arg2
:传递的其他参数
⚠️注意:返回由指定的this值和初始化参数改造的原函数拷贝(新函数),因此当我们只是想改变this 指向,并且不想调用这个函数的时候,可以使用bind,比如改变定时器内部的this
指向。
const obj = {age:18}
function fn(){console.log(this)}
//1.bind不会调用函数
//2.能改变this指向
//3.返回值是个函数,但是这个函数里面的this是更改过的obj
const fun = fn.bind(obj)
总结
call, apply, bind 总结
相同点:
都可以改变函数内部的 this 指向。
区别点:
call 和 apply 会调用函数,并且改变函数内部 this 指向。
call 和 apply 传递的参数不一样,call 传递参数 arg1, arg2... 形式,apply 必须数组形式 [arg]。
bind 不会调用函数,可以改变函数内部 this 指向。
主要应用场景:
call 调用函数并且可以传递参数。
apply 经常跟数组有关系,比如借助于 Math 对象实现数组最大值最小值。
bind 不调用函数,但是还想改变 this 指向,比如改变定时器内部的 this 指向。
性能优化
防抖和节流总结:
防抖
所谓防抖,就是指触发事件后在 n 秒内函数只能执行一次,如果在 n 秒内又触发了事件,则会重新计算函数执行时间。
举个栗子:王者荣耀回城,只要被打断就需要重新来
使用场景:
搜索框搜索输入。只需用户最后一次输入完,再发送请求
手机号、邮箱验证输入检测
lodash防抖
语法:_.debounce(fun,时间)
const box = document.querySelector('.box')
let i = 1
function mouseMove(){
box.innerHTML = i++
//如果里面存在大量消耗能的代码,比如dom操作,比如数据处理,可能造成卡顿
}
//添加事件
//剥削。addEventListener('mousemove',mouseMove)
//利用Lodash库实现防抖
box.addEventListener('mousemove',_.debounce(mouseMove,500))
手写防抖*
核心思路:防抖的核心就是利用定时器(setTimeout)来实现的
声明一个定时器变量
当鼠标每次滑动都先判断是否有定时器,如果有定时器先清除以的定时器
如果没有定时器则开启定时器,同时存到变量里面
在定时器里面调用要执行的函数
function debounce(fn, wait) {
let timeoutId = null; // 保存定时器ID
// 返回一个新的函数
return function() {
const context = this; // 保存函数调用的上下文
const args = arguments; // 保存函数调用的参数
// 清除之前的定时器
if (timeoutId) {
clearTimeout(timeoutId);
}
// 设置一个新的定时器
timeoutId = setTimeout(() => {
fn.apply(context, args); // 执行原函数
}, wait);
};
}
在这个例子中,debounce 函数接受两个参数:fn 是需要执行的函数,wait 是延迟执行的时间(以毫秒为单位)。每次触发事件时,都会重置定时器,只有在最后一次触发事件后 wait 时间内没有再次触发事件时,fn 函数才会执行。这样就可以避免在快速连续触发事件时频繁调用 fn 函数。
节流
所谓节流,就是指连续触发事件但是在 n 秒中只执行一次函数
举个栗子:和平精英98k换子弹期间不能射击
使用场景:高频事件:鼠标移动mousemove、页面尺寸缩放resize、滚动条滚动scroll等等
lodash节流
语法:_.throttle(fun,时间)
手写节流*
节流的核心就是利用定时器(setTimeout)来实现