JavaScript进阶
作用域
作用域(scope)规定了变量能够被访问的 范围,离开了这个范围变量便不能被访问。作用域分为 全局作用域和局部作用域
局部作用域
局部作用域分为函数作用域和块作用域。
函数作用域
在函数内部声明的变量只能在函数内部被访问,外部无法直接访问。
1 | <script> |
总结:
- 函数内部声明的变量,在函数外部无法被访问
- 函数的参数也是函数内部的局部变量
- 不同函数内部声明的变量无法互相访问
- 函数执行完毕后,函数内部的变量实际被清空了
块作用域
在 JavaScript 中使用 {}
包裹的代码称为代码块,代码块内部声明的变量外部将【有可能】无法被访问。
1 | <script> |
JavaScript 中除了变量外还有常量,常量与变量本质的区别是【常量必须要有值且不允许被重新赋值】,常量值为对象时其属性和方法允许重新赋值。
1 | <script> |
总结:
let
声明的变量会产生块作用域,var
不会产生块作用域const
声明的常量也会产生块作用域- 不同代码块之间的变量无法互相访问
- 推荐使用
let
或const
注:开发中 let
和 const
经常不加区分的使用,如果担心某个值会不小被修改时,则只能使用 const
声明成常量。
全局作用域
<script>
标签和 .js
文件的【最外层】就是所谓的全局作用域,在此声明的变量在函数内部也可以被访问。
1 | <script> |
全局作用域中声明的变量,任何其它作用域都可以被访问,如下代码所示:
1 | <script> |
总结:
- 为
window
对象动态添加的属性默认也是全局的,不推荐! - 函数中未使用任何关键字声明的变量为全局变量,不推荐!!!
- 尽可能少的声明全局变量,防止全局变量被污染
JavaScript 中的作用域是程序被执行时的底层机制,了解这一机制有助于规范代码书写习惯,避免因作用域导致的语法错误。
作用域链
在解释什么是作用域链前先来看一段代码:
1 | <script> |
函数内部允许创建新的函数,f
函数内部创建的新函数 g
,会产生新的函数作用域,由此可知作用域产生了嵌套的关系。
如下图所示,父子关系的作用域关联在一起形成了链状的结构,作用域链的名字也由此而来。
作用域链本质上是底层的变量查找机制,在函数被执行时,会优先查找当前函数作用域中查找变量,如果当前作用域查找不到则会依次逐级查找父级作用域直到全局作用域,如下代码所示:
1 | <script> |
作用域链是 JavaScript 中的一个重要概念,它是指在执行上下文中,所有可访问变量和函数的集合。当在当前执行上下文中访问一个变量或函数时,JavaScript 引擎会先从当前执行上下文的变量对象中查找,如果没有找到,则会继续在外层执行上下文的变量对象中查找,直到找到该变量或函数为止。这个查找的过程就构成了作用域链。
下面是一个简单的示例,说明作用域链的查找过程:
1 | function outer() { |
在上面的代码中,我们定义了一个 outer
函数和一个 inner
函数。outer
函数内定义了一个变量 a
,然后返回 inner
函数。在全局作用域中,我们调用 outer
函数并将其返回值赋值给 fn
,然后再调用 fn
函数。
在 fn
函数中,我们访问了变量 a
。由于 a
不在当前执行上下文的变量对象中,JavaScript 引擎会继续在外层执行上下文的变量对象中查找,即在 outer
函数的变量对象中查找。最终,JavaScript 引擎找到了变量 a
并输出了其值。
需要注意的是,作用域链是在函数定义时就已经确定的,而不是在函数调用时。每当函数被调用时,都会创建一个新的执行上下文,并将其压入执行上下文栈中。每个执行上下文都有自己的作用域链,因此在不同的执行上下文中,同名的变量或函数可能会指向不同的对象。
总结:
- 嵌套关系的作用域串联起来形成了作用域链
- 相同作用域链中按着从小到大的规则查找变量
- 子作用域能够访问父作用域,父级作用域无法访问子级作用域
闭包
闭包是 JavaScript 中的一个重要概念,它是指一个函数和其周围的变量构成的一个封闭的作用域。这个封闭的作用域使得函数内部的变量在函数外部不可访问,但是如果函数内部定义的函数引用了这些变量,那么这些变量就会被保留下来,形成了一个闭包。
下面是一个使用闭包的示例:
1 | function outer() { |
在上面的代码中,我们定义了一个 outer
函数和一个 inner
函数。在 outer
函数中,我们定义了一个变量 a
,然后返回了 inner
函数。在全局作用域中,我们调用 outer
函数并将其返回值赋值给 fn
,然后再调用 fn
函数。
在 fn
函数中,我们访问了变量 a
。由于 a
不在 fn
函数的作用域中,但是由于 fn
函数是由 outer
函数返回的,而 outer
函数的执行上下文中包含了变量 a
,因此变量 a
仍然可以被访问到,这就形成了一个闭包。
总结:
1.怎么理解闭包?
- 闭包 = 内层函数 + 外层函数的变量
2.闭包的作用?
- 封闭数据,实现数据私有,外部也可以访问函数内部的变量
- 闭包很有用,因为它允许将函数与其所操作的某些数据(环境)关联起来
3.闭包可能引起的问题?
- 内存泄漏
变量提升
变量提升是 JavaScript 中比较“奇怪”的现象,它允许在变量声明之前即被访问,
1 | <script> |
总结:
- 变量在未声明即被访问时会报语法错误
- 变量在声明之前即被访问,变量的值为
undefined
let
声明的变量不存在变量提升,推荐使用let
- 变量提升出现在相同作用域当中
- 实际开发中推荐先声明再访问变量
注:关于变量提升的原理分析会涉及较为复杂的词法分析等知识,而开发中使用 let
可以轻松规避变量的提升,因此在此不做过多的探讨,有兴趣可查阅资料 。
函数
知道函数参数默认值、动态参数、剩余参数的使用细节,提升函数应用的灵活度,知道箭头函数的语法及与普通函数的差异。
函数提升
函数提升与变量提升比较类似,是指函数在声明之前即可被调用。
1 | <script> |
总结:
- 函数提升能够使函数的声明调用更灵活
- 函数表达式不存在提升的现象
- 函数提升出现在相同作用域当中
函数参数
函数参数的使用细节,能够提升函数应用的灵活度。
默认值
1 | <script> |
总结:
- 声明函数时为形参赋值即为参数的默认值
- 如果参数未自定义默认值时,参数的默认值为
undefined
- 调用函数时没有传入对应实参时,参数的默认值被当做实参传入
动态参数
arguments
是函数内部内置的伪数组变量,它包含了调用函数时传入的所有实参。
1 | <script> |
总结:
arguments
是一个伪数组arguments
的作用是动态获取函数的实参
剩余参数
1 | <script> |
总结:
...
是语法符号,置于最末函数形参之前,用于获取多余的实参- 借助
...
获取的剩余实参,是个真数组
箭头函数
箭头函数是一种声明函数的简洁语法,它与普通函数并无本质的区别,差异性更多体现在语法格式上。
1 | <body> |
总结:
- 箭头函数属于表达式函数,因此不存在函数提升
- 箭头函数只有一个参数时可以省略圆括号
()
- 箭头函数函数体只有一行代码时可以省略花括号
{}
,并自动做为返回值被返回
箭头函数参数
箭头函数中没有 arguments
,只能使用 ...
动态获取实参
1 | <body> |
箭头函数 this
箭头函数不会创建自己的this,它只会从自己的作用域链的上一层沿用this。
1 | <script> |
解构赋值
知道解构的语法及分类,使用解构简洁语法快速为变量赋值。
解构赋值是一种快速为变量赋值的简洁语法,本质上仍然是为变量赋值,分为数组解构、对象解构两大类型。
数组解构
数组解构是将数组的单元值快速批量赋值给一系列变量的简洁语法,如下代码所示:
1 | <script> |
总结:
- 赋值运算符
=
左侧的[]
用于批量声明变量,右侧数组的单元值将被赋值给左侧的变量 - 变量的顺序对应数组单元值的位置依次进行赋值操作
- 变量的数量大于单元值数量时,多余的变量将被赋值为
undefined
- 变量的数量小于单元值数量时,可以通过
...
获取剩余单元值,但只能置于最末位 - 允许初始化变量的默认值,且只有单元值为
undefined
时默认值才会生效
注:支持多维解构赋值,比较复杂后续有应用需求时再进一步分析
对象解构
对象解构是将对象属性和方法快速批量赋值给一系列变量的简洁语法,如下代码所示:
1 | <script> |
总结:
- 赋值运算符
=
左侧的{}
用于批量声明变量,右侧对象的属性值将被赋值给左侧的变量 - 对象属性的值将被赋值给与属性名相同的变量
- 对象中找不到与变量名一致的属性时变量值为
undefined
- 允许初始化变量的默认值,属性不存在或单元值为
undefined
时默认值才会生效
注:支持多维解构赋值
1 | <body> |
综合案例
forEach遍历数组
forEach() 方法用于调用数组的每个元素,并将元素传递给回调函数
注意:
1.forEach 主要是遍历数组
2.参数当前数组元素是必须要写的, 索引号可选。
1 | <body> |
filter筛选数组
filter() 方法创建一个新的数组,新数组中的元素是通过检查指定数组中符合条件的所有元素
主要使用场景: 筛选数组符合条件的元素,并返回筛选之后元素的新数组
1 | <body> |
了解面向对象编程的基础概念及构造函数的作用,体会 JavaScript 一切皆对象的语言特征,掌握常见的对象属性和方法的使用。
- 了解面向对象编程中的一般概念
- 能够基于构造函数创建对象
- 理解 JavaScript 中一切皆对象的语言特征
- 理解引用对象类型值存储的的特征
- 掌握包装类型对象常见方法的使用
深入对象
了解面向对象的基础概念,能够利用构造函数创建对象。
构造函数
构造函数是专门用于创建对象的函数,如果一个函数使用 new
关键字调用,那么这个函数就是构造函数。
1 | <script> |
总结:
- 使用
new
关键字调用函数的行为被称为实例化 - 实例化构造函数时没有参数时可以省略
()
- 构造函数的返回值即为新创建的对象
- 构造函数内部的
return
返回的值无效!
注:实践中为了从视觉上区分构造函数和普通函数,习惯将构造函数的首字母大写。
实例成员
通过构造函数创建的对象称为实例对象,实例对象中的属性和方法称为实例成员。
1 | <script> |
总结:
- 构造函数内部
this
实际上就是实例对象,为其动态添加的属性和方法即为实例成员 - 为构造函数传入参数,动态创建结构相同但值不同的对象
注:构造函数创建的实例对象彼此独立互不影响。
静态成员
在 JavaScript 中底层函数本质上也是对象类型,因此允许直接为函数动态添加属性或方法,构造函数的属性和方法被称为静态成员。
1 | <script> |
总结:
- 静态成员指的是添加到构造函数本身的属性和方法
- 一般公共特征的属性或方法静态成员设置为静态成员
- 静态成员方法中的
this
指向构造函数本身
内置构造函数
掌握各引用类型和包装类型对象属性和方法的使用。
在 JavaScript 中最主要的数据类型有 6 种,分别是字符串、数值、布尔、undefined、null 和 对象,常见的对象类型数据包括数组和普通对象。其中字符串、数值、布尔、undefined、null 也被称为简单类型或基础类型,对象也被称为引用类型。
在 JavaScript 内置了一些构造函数,绝大部的数据处理都是基于这些构造函数实现的,JavaScript 基础阶段学习的 Date
就是内置的构造函数。
1 | <script> |
甚至字符串、数值、布尔、数组、普通对象也都有专门的构造函数,用于创建对应类型的数据。
Object
Object
是内置的构造函数,用于创建普通对象。
1 | <script> |
总结:
- 推荐使用字面量方式声明对象,而不是
Object
构造函数 Object.assign
静态方法创建新的对象Object.keys
静态方法获取对象中所有属性Object.values
表态方法获取对象中所有属性值
Array
Array
是内置的构造函数,用于创建数组。
1 | <script> |
数组赋值后,无论修改哪个变量另一个对象的数据值也会相当发生改变。
总结:
推荐使用字面量方式声明数组,而不是
Array
构造函数实例方法
forEach
用于遍历数组,替代for
循环 (重点)实例方法
filter
过滤数组单元值,生成新数组(重点)实例方法
map
迭代原数组,生成新数组(重点)实例方法
join
数组元素拼接为字符串,返回字符串(重点)实例方法
find
查找元素, 返回符合测试条件的第一个数组元素值,如果没有符合条件的则返回 undefined(重点)实例方法
every
检测数组所有元素是否都符合指定条件,如果所有元素都通过检测返回 true,否则返回 false(重点)实例方法
some
检测数组中的元素是否满足指定条件 如果数组中有元素满足条件返回 true,否则返回 false实例方法
concat
合并两个数组,返回生成新数组实例方法
sort
对原数组单元值排序实例方法
splice
删除或替换原数组单元实例方法
reverse
反转数组实例方法
findIndex
查找元素的索引值
包装类型
在 JavaScript 中的字符串、数值、布尔具有对象的使用特征,如具有属性和方法,如下代码举例:
1 | <script> |
之所以具有对象特征的原因是字符串、数值、布尔类型数据是 JavaScript 底层使用 Object 构造函数“包装”来的,被称为包装类型。
String
String
是内置的构造函数,用于创建字符串。
1 | <script> |
总结:
- 实例属性
length
用来获取字符串的度长(重点) - 实例方法
split('分隔符')
用来将字符串拆分成数组(重点) - 实例方法
substring(需要截取的第一个字符的索引[,结束的索引号])
用于字符串截取(重点) - 实例方法
startsWith(检测字符串[, 检测位置索引号])
检测是否以某字符开头(重点) - 实例方法
includes(搜索的字符串[, 检测位置索引号])
判断一个字符串是否包含在另一个字符串中,根据情况返回 true 或 false(重点) - 实例方法
toUpperCase
用于将字母转换成大写 - 实例方法
toLowerCase
用于将就转换成小写 - 实例方法
indexOf
检测是否包含某字符 - 实例方法
endsWith
检测是否以某字符结尾 - 实例方法
replace
用于替换字符串,支持正则匹配 - 实例方法
match
用于查找字符串,支持正则匹配
注:String 也可以当做普通函数使用,这时它的作用是强制转换成字符串数据类型。
Number
Number
是内置的构造函数,用于创建数值。
1 | <script> |
总结:
- 推荐使用字面量方式声明数值,而不是
Number
构造函数 - 实例方法
toFixed
用于设置保留小数位的长度
了解构造函数原型对象的语法特征,掌握 JavaScript 中面向对象编程的实现方式,基于面向对象编程思想实现 DOM 操作的封装。
- 了解面向对象编程的一般特征
- 掌握基于构造函数原型对象的逻辑封装
- 掌握基于原型对象实现的继承
- 理解什么原型链及其作用
- 能够处理程序异常提升程序执行的健壮性
编程思想
学习 JavaScript 中基于原型的面向对象编程序的语法实现,理解面向对象编程的特征。
面向过程
面向过程就是分析出解决问题所需要的步骤,然后用函数把这些步骤一步一步实现,使用的时候再一个一个的依次
调用就可以了。
举个栗子:蛋炒饭
面向对象
面向对象是把事务分解成为一个个对象,然后由对象之间分工与合作。
在面向对象程序开发思想中,每一个对象都是功能中心,具有明确分工。
面向对象编程具有灵活、代码可复用、容易维护和开发的优点,更适合多人合作的大型软件项目。
面向对象的特性:
封装性
继承性
多态性
构造函数
对比以下通过面向对象的构造函数实现的封装:
1 | <script> |
封装是面向对象思想中比较重要的一部分,js面向对象可以通过构造函数实现的封装。
同样的将变量和函数组合到了一起并能通过 this 实现数据的共享,所不同的是借助构造函数创建出来的实例对象之
间是彼此不影响的
总结:
- 构造函数体现了面向对象的封装特性
- 构造函数实例创建的对象彼此独立、互不影响
封装是面向对象思想中比较重要的一部分,js面向对象可以通过构造函数实现的封装。
前面我们学过的构造函数方法很好用,但是 存在浪费内存
的问题
原型对象
在 JavaScript 中,每个对象都有一个原型对象(prototype),它是一个普通的对象,可以包含属性和方法。当我们访问一个对象的属性或方法时,如果该对象本身没有这个属性或方法,那么就会去它的原型对象中查找,如果原型对象中也没有找到,就会继续去原型对象的原型对象中查找,直到找到为止。
JavaScript 中的原型对象形成了一个原型链,它是由一系列原型对象构成的链式结构,每个对象都有一个指向其原型对象的指针(也称为隐式原型),通过这个指针,我们可以访问到原型对象中的属性和方法。
下面是一个使用原型对象的示例:
1 | function Person(name, age) { |
在上面的代码中,我们定义了一个 Person
构造函数和一个 person
对象。在 Person
构造函数中,我们定义了两个属性 name
和 age
,然后通过原型对象给 Person
对象添加了一个方法 sayHi
。在 person
对象中,我们调用了 sayHi
方法,输出了 person
对象的姓名和年龄。
需要注意的是,JavaScript 中的每个函数都有一个原型对象,而普通的对象没有。当我们使用 new
操作符创建一个对象时,该对象就会继承构造函数的原型对象。因此,可以通过在构造函数的原型对象中添加属性和方法,来实现所有实例对象之间共享属性和方法的效果。
构造函数通过原型分配的函数是所有对象所 共享的。
- JavaScript 规定,每一个构造函数都有一个 prototype 属性,指向另一个对象,所以我们也称为原型对象
- 这个对象可以挂载函数,对象实例化不会多次创建原型上函数,节约内存
- 我们可以把那些不变的方法,直接定义在 prototype 对象上,这样所有对象的实例就可以共享这些方法。
- 构造函数和原型对象中的this 都指向 实例化的对象
1 | <script> |
了解了 JavaScript 中构造函数与原型对象的关系后,再来看原型对象具体的作用,如下代码所示:
1 | <script> |
构造函数 Person
中未定义任何方法,这时实例对象调用了原型对象中的方法 sayHi
,接下来改动一下代码:
1 | <script> |
构造函数 Person
中定义与原型对象中相同名称的方法,这时实例对象调用则是构造函中的方法 sayHi
。
通过以上两个简单示例不难发现 JavaScript 中对象的工作机制:当访问对象的属性或方法时,先在当前实例对象是查找,然后再去原型对象查找,并且原型对象被所有实例共享。
1 | <script> |
总结:结合构造函数原型的特征,实际开发重往往会将封装的功能函数添加到原型对象中。
constructor 属性
在哪里? 每个原型对象里面都有个constructor 属性(constructor 构造函数)
作用:该属性指向该原型对象的构造函数, 简单理解,就是指向我的爸爸,我是有爸爸的孩子
使用场景:
如果有多个对象的方法,我们可以给原型对象采取对象形式赋值.
但是这样就会覆盖构造函数原型对象原来的内容,这样修改后的原型对象 constructor 就不再指向当前构造函数了
此时,我们可以在修改后的原型对象中,添加一个 constructor 指向原来的构造函数。
对象原型
对象原型是指 JavaScript 中的每个对象都有一个原型对象(prototype),它是一个普通的对象,可以包含属性和方法。当我们访问一个对象的属性或方法时,如果该对象本身没有这个属性或方法,那么就会去它的原型对象中查找,如果原型对象中也没有找到,就会继续去原型对象的原型对象中查找,直到找到为止。
对象原型在 JavaScript 中非常重要,它可以用于实现对象之间的继承关系。通过在一个对象的原型对象中添加属性和方法,可以让该对象的所有实例对象都继承这些属性和方法。同时,对象原型也可以被用于实现对象的多态性,通过在不同的对象原型中定义同名的方法,可以让不同的对象实现相同的接口,从而实现多态性。
下面是一个使用对象原型实现继承的示例:
1 | function Animal(name) { |
在上面的代码中,我们定义了一个 Animal
构造函数和一个 Dog
构造函数。在 Animal
构造函数中,我们定义了一个属性 name
和一个方法 sayHi
。在 Dog
构造函数中,我们通过 Animal.call(this, name)
调用了 Animal
构造函数,并将 this
对象传递给它,从而继承了 Animal
构造函数的属性。然后,通过 Object.create(Animal.prototype)
创建了一个新对象,该对象的原型对象指向 Animal.prototype
,然后将该对象赋值给 Dog.prototype
,从而继承了 Animal.prototype
中的方法。最后,我们定义了 Dog
对象自己的方法 bark
。在 dog
对象中,我们调用了 sayHi
和 bark
方法,分别输出了狗的名字和叫声。
需要注意的是,JavaScript 中没有类的概念,因此对象原型是 JavaScript 中实现继承和多态性的重要手段。
对象都会有一个属性 proto 指向构造函数的 prototype 原型对象,之所以我们对象可以使用构造函数 prototype
原型对象的属性和方法,就是因为对象有 proto 原型的存在。
注意:
- proto 是JS非标准属性
- [[prototype]]和__proto__意义相同
- 用来表明当前实例对象指向哪个原型对象prototype
- __proto__对象原型里面也有一个 constructor属性,指向创建该实例对象的构造函数
原型继承
继承是面向对象编程的另一个特征,通过继承进一步提升代码封装的程度,JavaScript 中大多是借助原型对象实现继承
的特性。
龙生龙、凤生凤、老鼠的儿子会打洞描述的正是继承的含义。
1 | <body> |
原型链
基于原型对象的继承使得不同构造函数的原型对象关联在一起,并且这种关联的关系是一种链状的结构,我们将原型对
象的链状结构关系称为原型链
1 | <body> |
① 当访问一个对象的属性(包括方法)时,首先查找这个对象自身有没有该属性。
② 如果没有就查找它的原型(也就是 __proto__指向的 prototype 原型对象)
③ 如果还没有就查找原型对象的原型(Object的原型对象)
④ 依此类推一直找到 Object 为止(null)
⑤ __proto__对象原型的意义就在于为对象成员查找机制提供一个方向,或者说一条路线
⑥ 可以使用 instanceof 运算符用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上
在 JavaScript 中,原型链是一种实现继承的方式,它通过继承父类的原型对象来实现对父类属性和方法的继承。
每个对象都有一个内部属性 [[Prototype]]
,它指向该对象的原型。原型是一个对象,它包含该对象共享的属性和方法。当我们访问一个对象的属性或方法时,如果该对象自身没有该属性或方法,那么就会从它的原型中查找,如果原型中还没有,就会继续往上查找,直到找到 Object.prototype 为止。
下面是一个使用原型链的例子:
1 | <script> |
在上面的例子中,我们定义了一个 Animal
父类,和一个 Dog
子类。我们通过 Object.create()
方法,将 Dog
的原型设置为 Animal
的原型,这样 Dog
就可以继承 Animal
的属性和方法。需要注意的是,我们还需要将 Dog.prototype.constructor
设置为 Dog
,以保证 Dog
的实例可以调用 Dog
的构造函数。
当我们调用 dog.sayName()
方法时,由于 dog
对象本身没有 sayName()
方法,因此会从它的原型 Dog.prototype
中查找,而 Dog.prototype
又是通过 Object.create(Animal.prototype)
创建的,因此会继续往上查找,最终找到 Animal.prototype
中的 sayName()
方法并调用它。
当我们调用 dog.sayBark()
方法时,由于 dog
对象本身没有 sayBark()
方法,因此会从它的原型 Dog.prototype
中查找,而 Dog.prototype
中恰好有 sayBark()
方法,因此直接调用它。
需要注意的是,原型链并不是一种完美的继承方式,它有一些缺点,例如继承的属性和方法是共享的,如果在子类中修改了某个属性或方法,那么所有的子类实例都会受到影响。
深浅拷贝
浅拷贝
首先浅拷贝和深拷贝只针对引用类型
浅拷贝:拷贝的是地址
常见方法:
- 拷贝对象:Object.assgin() / 展开运算符 {…obj} 拷贝对象
- 拷贝数组:Array.prototype.concat() 或者 […arr]
如果是简单数据类型拷贝值,引用数据类型拷贝的是地址 (简单理解: 如果是单层对象,没问题,如果有多层就有问题)
深拷贝
首先浅拷贝和深拷贝只针对引用类型
深拷贝:拷贝的是对象,不是地址
深拷贝是指将一个对象完整地复制一份,包括其所有的属性和嵌套对象的属性,而不是只复制其引用。深拷贝在 JavaScript 中非常常用,可以用于避免对象引用造成的问题。
下面是一个使用递归实现深拷贝的示例:
1 | function deepCopy(obj) { |
在上面的代码中,我们定义了一个 deepCopy
函数,用于实现深拷贝。在函数中,我们首先判断了 obj
的类型,如果是基本数据类型或 null
,则直接返回。如果是对象或数组,则创建一个新的对象或数组,然后遍历 obj
中的所有属性,递归调用 deepCopy
函数,将属性值复制到新的对象或数组中。最后返回新的对象或数组。
在示例中,我们首先定义了一个对象 obj1
,然后通过 deepCopy
函数对其进行深拷贝,得到了一个新的对象 obj2
。在 obj2
中,我们修改了 a
属性的值,向数组 b
中添加了一个元素,并修改了嵌套对象 c.e.f
的值。最后,我们输出了 obj1
和 obj2
的值,可以看到它们完全独立,修改 obj2
不会对 obj1
产生影响。
需要注意的是,深拷贝可能会比较耗时,因为需要递归遍历对象的所有属性。在实际应用中,我们需要根据对象的大小和复杂度来判断是否需要使用深拷贝。
常见方法:
- 通过递归实现深拷贝
- lodash/cloneDeep
- 通过JSON.stringify()实现
递归实现深拷贝
函数递归:
如果一个函数在内部可以调用其本身,那么这个函数就是递归函数
- 简单理解:函数内部自己调用自己, 这个函数就是递归函数
- 递归函数的作用和循环效果类似
- 由于递归很容易发生“栈溢出”错误(stack overflow),所以必须要加退出条件 return
1 | <body> |
js库lodash里面cloneDeep内部实现了深拷贝
1 | <body> |
JSON序列化
1 | <body> |
异常处理
了解 JavaScript 中程序异常处理的方法,提升代码运行的健壮性。
throw
异常处理是指预估代码执行过程中可能发生的错误,然后最大程度的避免错误的发生导致整个程序无法继续运行
总结:
- throw 抛出异常信息,程序也会终止执行
- throw 后面跟的是错误提示信息
- Error 对象配合 throw 使用,能够设置更详细的错误信息
1 | <script> |
总结:
throw
抛出异常信息,程序也会终止执行throw
后面跟的是错误提示信息Error
对象配合throw
使用,能够设置更详细的错误信息
try … catch
1 | <script> |
总结:
try...catch
用于捕获错误信息- 将预估可能发生错误的代码写在
try
代码段中 - 如果
try
代码段中出现错误后,会执行catch
代码段,并截获到错误信息
debugger
相当于断点调试
处理this
了解函数中 this 在不同场景下的默认值,知道动态指定函数 this 值的方法。
this
是 JavaScript 最具“魅惑”的知识点,不同的应用场合 this
的取值可能会有意想不到的结果,在此我们对以往学习过的关于【 this
默认的取值】情况进行归纳和总结。
普通函数
普通函数的调用方式决定了 this
的值,即【谁调用 this
的值指向谁】,如下代码所示:
1 | <script> |
注: 普通函数没有明确调用者时 this
值为 window
,严格模式下没有调用者时 this
的值为 undefined
。
箭头函数
箭头函数中的 this
与普通函数完全不同,也不受调用方式的影响,事实上箭头函数中并不存在 this
!箭头函数中访问的 this
不过是箭头函数所在作用域的 this
变量。
1 | <script> |
在开发中【使用箭头函数前需要考虑函数中 this
的值】,事件回调函数使用箭头函数时,this
为全局的 window
,因此DOM事件回调函数不推荐使用箭头函数,如下代码所示:
1 | <script> |
同样由于箭头函数 this
的原因,基于原型的面向对象也不推荐采用箭头函数,如下代码所示:
1 | <script> |
改变this指向
以上归纳了普通函数和箭头函数中关于 this
默认值的情形,不仅如此 JavaScript 中还允许指定函数中 this
的指向,有 3 个方法可以动态指定普通函数中 this
的指向:
call
使用 call
方法调用函数,同时指定函数中 this
的值,使用方法如下代码所示:
1 | <script> |
总结:
call
方法能够在调用函数的同时指定this
的值- 使用
call
方法调用函数时,第1个参数为this
指定的值 call
方法的其余参数会依次自动传入函数做为函数的参数
apply
使用 call
方法调用函数,同时指定函数中 this
的值,使用方法如下代码所示:
1 | <script> |
总结:
apply
方法能够在调用函数的同时指定this
的值- 使用
apply
方法调用函数时,第1个参数为this
指定的值 apply
方法第2个参数为数组,数组的单元值依次自动传入函数做为函数的参数
bind
bind
方法并不会调用函数,而是创建一个指定了 this
值的新函数,使用方法如下代码所示:
1 | <script> |
注:bind
方法创建新的函数,与原函数的唯一的变化是改变了 this
的值。
防抖节流
- 防抖(debounce)
所谓防抖,就是指触发事件后在 n 秒内函数只能执行一次,如果在 n 秒内又触发了事件,则会重新计算函数执行时间 - 节流(throttle)
所谓节流,就是指连续触发事件但是在 n 秒中只执行一次函数
防抖
防抖是一种常见的前端性能优化方法,它可以避免在某些频繁事件中过度触发回调函数而导致界面卡顿的问题。防抖的原理是在事件触发后,延迟一定时间再执行回调函数,如果在这段时间内又触发了事件,则重新计时,直到没有再触发事件,才最终执行回调函数。
下面是一个简单的防抖函数的实现:
1 | function debounce(fn, delay) { |
在上面的代码中,我们定义了一个 debounce
函数,用于实现防抖。在函数中,我们使用闭包保存了一个 timer
变量,用于记录定时器的 ID。当事件触发时,我们首先清除之前的定时器,然后创建一个新的定时器,在一定时间后执行回调函数。如果在这段时间内又触发了事件,我们就会重新计时,直到时间到达后再执行回调函数。
在示例中,我们使用 addEventListener
方法给一个输入框绑定了 input
事件,并传入了一个防抖后的回调函数。当用户在输入框中输入时,会触发 input
事件,但由于我们使用了防抖函数,所以只有在用户停止输入一段时间后,才会执行回调函数。
需要注意的是,防抖函数的实现有很多种,上面的示例只是其中一种。在实际应用中,我们需要根据具体的场景和需求,选择合适的实现方式。
节流
JavaScript 中的“节流”是一种优化技术,可用于限制函数的调用频率。它通常用于处理那些频繁触发事件的情况,如滚动、拖拽、调整窗口大小等等。
“节流”技术的基本思路是:在一段时间内,只让函数被调用一次,而不是立即执行每一次事件触发。这样可以减少浏览器的负担,提高网页的性能。
以下是一个简单的 JavaScript 节流函数的实现示例:
1 | function throttle(func, wait) { |
这个函数接受两个参数:要节流的函数和节流的时间间隔(以毫秒为单位)。它返回一个新的函数,这个新函数会在时间间隔内最多只被调用一次。
例如,如果你有一个需要节流的函数 scrollHandler
,你可以这样使用 throttle
函数:
1 | const throttledScrollHandler = throttle(scrollHandler, 100); |
这将创建一个新的函数 throttledScrollHandler
,它将限制 scrollHandler
函数的调用频率为每 100 毫秒一次。
- Title: JavaScript进阶
- Author: cccs7
- Created at: 2023-07-03 11:19:12
- Updated at: 2023-07-03 20:34:26
- Link: https://blog.cccs7.icu/2023/07/03/JavaScript进阶/
- License: This work is licensed under CC BY-NC-SA 4.0.