javascript 进阶
作用域
作用域规定了变量能够被访问的范围,离开这个范围变量就不能被访问
作用域分为:
局部作用域
局部作用域分为函数作用域和块作用域。
函数作用域:
在函数内部声明的变量只能在函数内部被访问,外部无法访问
总结:
- 函数内部声明的变量,在函数外部无法被访问
- 函数的参数也是函数内部的局部的变量
- 不同函数内部声明的变量无法相互访问
- 函数执行完毕后,函数的变量实际被清空了
块作用域:
在JavaScript中使用
{}
包裹的代码块内部声明的变量外部将有可能无法被访问
for(let i=0;i<10;i++){
// i 只能的该代码块中被访问
console.log(i)
}
// 超出了t的作用域 ,报错
console.log(i)
总结:
let
声明的变量,const
声明的常量,都会产生块级作用域var
声明的变量不会产生块级作用域- 不同代码块之间的变量无法相互访问
- 推荐使用
let
和const
全局作用域
在
<script>
标签 和.js
文件的最外层就是全局作用域,在此声明的变量在函数内部也可以访问,全局作用域下声明的变量,任何其他作用域都可以被访问
//全局作用域,下声明变量num
const num=10
function fn(){
//函数内部可以使用
console.log(num)
}
注意:
- 为 window 对象动态添加的属性默认也是全局的,不推荐
- 函数未使用任何关键字声明的变量为全局变量,不推荐
- 尽可能少的声明全局变量,防止全局变量被污染
作用域链
作用域链本质是底层的变量查找机制
- 函数在被执行时,会优先找当前函数作用域中查找变量
- 如果当前作用域查找不到则会依次逐级查找父级作用域直到全局作用域
//全局作用域
let a = 1
let b = 2
//局部作用域
function f() {
let a = 1
function g() {
a = 2
console.log(a) // 2
}
g()
}
f()
总结:
- 嵌套更新的作用域串联起来形成了作用域链
- 相同作用域链中按着从小到大的规则查找变量
- 子作用域能够访问父级作用域,父级作用域无法访问子级作用域
JS 的垃圾回收机制
垃圾回收机制 (Garbage Collection) 简称 GC
JS 中的内存的分配和回收都是自动完成的,,内存在不使用的时候会被垃圾回收器自动回收。
但如果不了解JS的内存管理机制,我们同样非常容易成内存泄漏(内存无法被回收)的情况
不再用到的内存,没有及时释放,就叫做内存泄漏
内存的生命周期 :
- 内存分配:当我们声明变量、函数、对象的时候,系统会自动为他们分配内存
- 内存使用:即读写内存,也就是使用变量、函数等
- 内存回收:使用完毕,由垃圾回收自动回收不再使用的内存
-
- 全局变量一般不会回收(关闭页面回收);
- 一般情况下局部变量的值, 不用了, 会被自动回收掉
垃圾回收算法说明
所谓垃圾回收, 核心思想就是如何判断内存是否已经不再会被使用了, 如果是, 就视为垃圾, 释放掉
下面介绍两种常见的浏览器垃圾回收算法: 引用计数法 和 标记清除法
引用计数
IE采用的引用计数算法, 定义“内存不再使用”的标准很简单,就是看一个对象是否有指向它的引用。
算法:
- 跟踪记录每个值被引用的次数。
- 如果这个值的被引用了一次,那么就记录次数1
- 多次引用会累加。
- 如果减少一个引用就减1。
- 如果引用次数是0 ,则释放内存。
引用计数算法是个简单有效的算法。
但它却存在一个致命的问题:嵌套引用。
如果两个对象相互引用,尽管他们已不再使用,垃圾回收器不会进行回收,导致内存泄露。
标记清除法
现代的浏览器已经不再使用引用计数算法了。
现代浏览器通用的大多是基于标记清除算法的某些改进算法,总体思想都是一致的。
核心:
- 标记清除算法将“不再使用的对象”定义为“无法达到的对象”。
- 就是从根部(在JS中就是全局对象)出发定时扫描内存中的对象。 凡是能从根部到达的对象,都是还需要使用的。
- 那些无法由根部出发触及到的对象被标记为不再使用,稍后进 行回收。
闭包
概念:一个函数对周围状态的引用捆绑在一起,内层函数中访问到其外层函数的作用域
简单理解:闭包 = 内层函数 + 外层函数的变量
function outer() {
const a = 1
function f(){
console.log(a)
}
f()
}
outer
// 内层函数调用外层函数的变量,就是闭包
作用: 封闭数据,提供操作,外部也可以访问函数内部的变量
基本格式:
闭包应用:实现数据的私有
比如,我们要做个统计函数调用次数,函数调用一次,就++
普通写法
let count = 1
function fn() {
count++
console.log(count)
}
fn() //2
fn() //3
这个count 是个全局变量,很容易被修改
闭包写法:
function fn() {
let count = 1
return function fun() {
count++
console.log(count)
}
}
const result =fn()
result() //2
result() //3
实现了数据私有,无法直接修改count
闭包可能引起的问题:内存泄漏
变量提升
变量提升是 JavaScript 中比较“奇怪”的现象,它允许在变量声明之前即被访问(仅存在于var声明变量)
console.log(str) //不报错 控制台显示undefined
var str = 'hello'
注意:
- 变量在未声明即被访问时会报语法错误
- 变量在var声明之前即被访问,变量的值为 undefined
- let / const 声明的变量不存在变量提升
- 变量提升出现在相同作用域当中
- 实际开发中推荐先声明再访问变量
函数进阶
函数提升
函数提升与变量提升比较类似,是指函数在声明之前即可被调用。
函数参数
-
动态参数
arguments 是函数内部内置的伪数组变量,它包含了调用函数时传入的所有实参
//求和函数 不管用户传入几个实参,都要把和求出来 function sum() { let s = 0 for(let i = 0;i < arguments.length;i++){ s += arguments[i] } return s } sum(5,10) //15 sum(1,2,3) //6
-
剩余参数
剩余参数允许我们将一个不定数量的参数表示为一个数组
...
是语法符号,置于最末函数形参之前,用于获取多余的实参- 借助
...
获取的剩余实参,是个真数组
function sum(...other) { let s = 0 for(let i = 0;i < other.length;i++){ s += other[i] } return s } sum(5,10) //15 sum(1,2,3) //6
区别:
- … 是语法符号,置于最末函数形参之前,用于获取多余的实参
- 借助 … 获取的剩余实参,是个真数组
展开运算符:...
展开运算符 (…), 将一个数组进行展开,不会修改原数组
const arr=[1,2,3,4,5]
console.log(...arr) //1 2 3 4 5
典型运用场景: 求数组最大值(最小值)、合并数组等
const arr=[1,2,3,6,4,5]
const arr2=[7,8,9]
//最大值
const max=Math.max(...arr)
//合拼
const arr3=[...arr,...arr2]
展开运算符 or 剩余参数
剩余参数:函数参数使用,得到真数组
展开运算符:数组中使用,数组展开
箭头函数
语法:
//普通函数
const fn = function (){
...
}
fn()
//1.箭头 无参数
const fn = () =>{
...
}
//2.只有一个参数
const fn = x=> {
return x
}
//3.函数体只有一行代码,可以写一行上,无需写 return 直接返回值
const fn = (x,y) => x + y //返回 x + y 的值
//加括号的函数体返回对象字面量表达式
const fn1 = uname =>({uname : uname})
console.log(fn1('叶湘伦')) //控制台:{uname:"叶湘伦"}
- 箭头函数属于表达式函数,因此不存在函数提升
- 箭头函数只有一个参数时可以省略圆括号 ()
- 箭头函数函数体只有一行代码时可以省略花括号 {},并自动做为返回值被
返回 - 加括号的函数体返回对象字面量表达式
箭头函数参数:
- 普通函数有
arguments
动态参数 - 箭头函数没有
arguments
动态参数,但是有 剩余参数..args
箭头函数 this:
在箭头函数出现之前,每一个新函数根据它是被如何调用的来定义这个函数的 this 值
箭头函数不会创建自己的 this ,它只会从自己的作用域链的上一层沿用 this 。
在开发中【使用箭头函数前需要考虑函数中 this 的值】,事件回调函数使用箭头函数时,this 为全局的 window,因此,DOM事件回调函数为了简便,还是不太推荐使用箭头函数
数组解构
数组解构是将数组的单元值快速批量赋值给一系列变量的简洁语法。
// 普通的数组
let arr = [1, 2, 3];
// 批量声明变量 a b c
// 同时将数组单元值 1 2 3 依次赋值给变量 a b c
let [a, b, c] = arr;
console.log(a); // 1
console.log(b); // 2
console.log(c); // 3
基本语法:
- 赋值运算符
=
左侧的[]
用于批量声明变量,右侧数组的单元值将被赋值给左侧的变量 - 变量的顺序对应数组单元值的位置依次进行赋值操作
- 变量的数量大于单元值数量时,多余的变量将被赋值为
undefined
- 变量的数量小于单元值数量时,可以通过
...
获取剩余单元值,但只能置于最末位 - 允许初始化变量的默认值,且只有单元值为
undefined
时默认值才会生效
对象解构
// 普通对象
const user = {
name: '小明',
age: 18
};
// 批量声明变量 name age
// 同时将数组单元值 小明 18 依次赋值给变量 name age
const {name, age} = user
console.log(name) // 小明
console.log(age) // 18
总结:
- 赋值运算符
=
左侧的{}
用于批量声明变量,右侧对象的属性值将被赋值给左侧的变量 - 对象属性的值将被赋值给与属性名相同的变量
- 对象中找不到与变量名一致的属性时变量值为
undefined
- 允许初始化变量的默认值,属性不存在或单元值为
undefined
时默认值才会生效
遍历数组 forEach 方法
forEach() 方法用于调用数组的每个元素,并将元素传递给回调函数
遍历数组.forEach(function (当前数组元素,索引号){
//函数体
})
注意:
- forEach 主要是遍历数组
- 参数当前数组元素是必须要写的, 索引号可选。
筛选数组 filter 方法
filter() 方法创建一个新的数组,新数组中的元素是通过检查指定数组中符合条件的所有元素
主要使用场景: 筛选数组符合条件的元素,并返回筛选之后元素的新数组
遍历数组.filter(function (当前数组元素,索引号){
return 筛选条件
})
//筛选数组大于30的元素
const arr=[10,26,62,61,56,12,36]
const re = arr.filter(function(item){
return item > 30
})
console.log(re) //[62,61,56,36]
返回值:返回数组,包含了符合条件的所有元素。如果没有符合条件的元素则返回空数组
参数:currentValue 必须写, index 可选
因为返回新数组,所以不会影响原数组
对象创建方法
-
利用字面量创建
const obj = { name:'叶湘伦', age:18 }
-
利用 new Object 创建对象
const obj = new Object({ name:'叶湘伦', age:18 })
-
利用构造函数创建
// 构造函数 function Obj(name,age){ this.name = name this.age = age } //创建一个对象 const Stu = new Obj('叶湘伦',18)
注意:
- 语法:大写字母开头的函数
- 约定:它们的命名以大写字母开头。只能由 “new” 操作符来执行。
说明:
- 使用 new 关键字调用函数的行为被称为实例化
- 实例化构造函数时没有参数可以省略()
- 构造函数内部不用写 return ,返回值即为新建的对象
- new Object() new Date() 也是实例化构造函数
实列化执行过程:
- 创建一个新空对象
- 构造函数 this 指向新对象
- 执行构造函数代码,修改 this ,添加新的属性
- 返回新的对象
实例对象
通过构造函数创建的对象称为实例对象,实例对象中的属性和方法称为实例成员
// 构造函数
function Obj(name,age){
//构造函数内部的 this 就是实例对象
//实例对象中动态添加属性
this.name = name
this.age = age
//实例对象中动态添加方法
this.sayHi = function () {
console.log('hi')
}
}
//实例化 ,Stu 是实列对象
// Stu 实际就是构造函数内部的 this
const Stu = new Obj('叶湘伦',18)
//访问实例属性
console.log(Stu.name)
//调用实列方法
Stu.sayHi()
说明:
- 实例对象的属性和方法即为实例成员
- 为构造函数传入参数,动态创建结构相同但值不同的对象
- 构造函数创建的实例对象彼此独立互不影响。
静态成员:
在 JavaScript 中底层函数本质上也是对象类型,因此允许直接为函数动态添加属性或方法,构造函数的属性和方法被称为静态成员。
// 构造函数
function Obj(name,age){
//实例成员
}
//静态属性
Obj.eyes = 2
Obj.arms = 2
//静态方法
Obj.walk = funtion () {
console.log('走路中')
//this 指向 person
console.log(this.eyes)
}
总结:
- 静态成员指的是添加到构造函数本身的属性和方法
- 一般公共特征的属性或方法静态成员设置为静态成员
- 静态成员方法中的
this
指向构造函数本身
内置构造函数
在 JavaScript 中最主要的数据类型有 6 种,分别是字符串、数值、布尔、undefined、null 和 对象,常见的对象类型数据包括数组和普通对象。其中字符串、数值、布尔、undefined、null 也被称为简单类型或基础类型,对象也被称为引用类型。
在 JavaScript 内置了一些构造函数,绝大部的数据处理都是基于这些构造函数实现的,JavaScript 基础阶段学习的 Date
就是内置的构造函数。
引用类型:
Object
:
三个常用静态方法(静态方法就是只有构造函数Object可以调用的)
Object.keys
静态方法获取对象中所有属性(键)
Object.values
静态方法获取对象中所有属性值
Object.assign
静态方法常用于对象拷贝
//Object.keys
const obj = {name:'叶湘伦',age:18}
const arr = Object.key(obj)
console.log(arr) // ['name','age'] 返回是一个数组
//Object.values
const arr2 =Object.values(obj)
console.log(arr2) // ['叶湘伦',18] 返回是一个数组
//Object.assign
const obj2 = {}
Object.assign(obj2,obj)
console.log(obj2) // {name:'叶湘伦',age:18}
//使用:经常使用的场景给对象添加属性
Object.assign(obj,{sex:'男'})
console.log(arr) // {name:'叶湘伦',age:18,sex:'男'}
Array
:
数组常见实例方法-核心方法
方法 | 作用 | 说明 |
---|---|---|
forEach | 遍历数组 | 不返回值,用于不改变值,经常用于查找打印输出值 |
filter | 过滤数组 | 筛选数组元素,并生成新数组 |
map | 迭代数组 | 返回新数组,新数组里面的元素是处理之后的值,经常用于处理数据 |
reduce | 累积器 | 返回函数累计处理的结果,经常用于求和等 |
总结:
-
推荐使用字面量方式声明数组,而不是
Array
构造函数 -
实例方法
forEach
用于遍历数组,替代for
循环 (重点) -
实例方法
filter
过滤数组单元值,生成新数组(重点) -
实例方法
map
迭代原数组,生成新数组(重点) -
实例方法
join
数组元素拼接为字符串,返回字符串(重点) -
实例方法
find
查找元素, 返回符合测试条件的第一个数组元素值,如果没有符合条件的则返回 undefined(重点) -
实例方法
every
检测数组所有元素是否都符合指定条件,如果所有元素都通过检测返回 true,否则返回 false(重点) -
实例方法
some
检测数组中的元素是否满足指定条件 如果数组中有元素满足条件返回 true,否则返回 false -
实例方法
concat
合并两个数组,返回生成新数组 -
实例方法
sort
对原数组单元值排序 -
实例方法
splice
删除或替换原数组单元 -
实例方法
reverse
反转数组 -
实例方法
findIndex
查找元素的索引值
String
:
总结:
- 实例属性
length
用来获取字符串的度长(重点) - 实例方法
split('分隔符')
用来将字符串拆分成数组(重点) - 实例方法
substring(需要截取的第一个字符的索引[,结束的索引号])
用于字符串截取(重点) - 实例方法
startsWith(检测字符串[, 检测位置索引号])
检测是否以某字符开头(重点) - 实例方法
includes(搜索的字符串[, 检测位置索引号])
判断一个字符串是否包含在另一个字符串中,根据情况返回 true 或 false(重点) - 实例方法
toUpperCase
用于将字母转换成大写 - 实例方法
toLowerCase
用于将就转换成小写 - 实例方法
indexOf
检测是否包含某字符 - 实例方法
endsWith
检测是否以某字符结尾 - 实例方法
replace
用于替换字符串,支持正则匹配 - 实例方法
match
用于查找字符串,支持正则匹配
注:String 也可以当做普通函数使用,这时它的作用是强制转换成字符串数据类型。
原型
JavaScript 规定,每一个构造函数都有一个 prototype 属性,指向另一个对象,所以我们也称为原型对象
作用:
-
共享方法
-
可以把那些不变的方法,直接定义在 prototype 对象上
constructor 属性: 指向该原型对象的构造函数
对象原型:
对象都会有一个属性 __proto__
指向构造函数的 prototype 原型对象,之所以我们对象可以使用构造函数 prototype 原型对象的属性和方法,就是因为对象有__proto__
原型的存在
总结:
prototype
是什么?哪里来的?
- 原型(原型对象)
- 构造函数都自动有原型
constructor
属性在哪里?作用干啥的?
-
prototype
原型和对象原型__proto__
里面都有 -
都指向创建实例对象/原型的构造函数
__proto__
属性在哪里?指向谁?
- 在实例对象里面
- 指向原型
prototype
原型继承
继承是面向对象编程的另一个特征,通过继承进一步提升代码封装的程度,JavaScript 中大多是借助原型对象实现继承的特性。
-
封装-抽取公共部分
- 把男人和女人公共的部分抽取出来放到人类里面
-
继承-让男人和女人都能继承人类的一些属性和方法
- 把男人女人公共的属性和方法抽取出来 People
- 然后赋值给Man的原型对象,可以共享这些属性和方法
- 注意让constructor指回Man这个构造函数
//人类
const People = {
head:1,
eyes:2,
leys:2,
say:function () {}
}
//男人
function Man(){
}
//公共属性和方法给原型
Man.prototype = People
//如果我们给男人添加了一个吸烟的方法,发现女人自动也添加这个方法
//男人和女人都同时使用了同一个对象,根据引用类型的特点,他们指向同一个对象,修改一个就会都影响
Man.prototype.smoking = function () {}
//女人
function Woamn() {
//独有方法
this.body =function () {}
}
// 继承写法完善 ,解决上面问题
//男人和女人不要使用同一个对象,但是不同对象里面包含相同的属性和方法
//答案:构造函数
//new 每次都会创建一个新的对象
function People() = {
head:1,
eyes:2,
leys:2,
say:function () {}
}
//男人
function Man(){
}
//公共属性和方法
Man.prototype =new People()
原型链:
基于原型对象的继承使得不同构造函数的原型对象关联在一起,并且这种关联的关系是一种链状的结构,我们将原型对象的链状结构关系称为原型链
原型链-查找规则:
- 当访问一个对象的属性(包括方法)时,首先查找这个对象自身有没有该属性。
- 如果没有就查找它的原型(也就是
__proto__
指向的prototype
原型对象) - 如果还没有就查找原型对象的原型(
Object
的原型对象) - 依此类推一直找到
Object
为止(null) __proto__
对象原型的意义就在于为对象成员查找机制提供一个方向,或者说一条路线
深浅拷贝
浅拷贝
拷贝的是地址
常见方法:
- 拷贝对象:
Object.assgin()
展开运算符{...obj}
拷贝对象 - 拷贝数组:
Array.prototype.concat()
或者[...arr]
总结:
- 直接赋值和浅拷贝有什么区别?
- 直接赋值的方法,只要是对象,都会相互影响,因为是直接拷贝对
象栈里面的地址 - 浅拷贝如果是一层对象,不相互影响,如果出现多层对象拷贝还会
相互影响
- 浅拷贝怎么理解?
- 拷贝对象之后,里面的属性值是简单数据类型直接拷贝值
- 如果属性值是引用数据类型则拷贝的是地址
深拷贝
拷贝的是对象,不是地址
常见方法:
- 通过递归实现深拷贝
- lodash/cloneDeep
- 通过JSON.stringify()实现
异常处理
throw 抛异常
总结:
throw
抛出异常信息,程序也会终止执行throw
后面跟的是错误提示信息Error
对象配合throw
使用,能够设置更详细的错误信息
try/catch
总结:
try...catch
用于捕获错误信息- 将预估可能发生错误的代码写在
try
代码段中 - 如果
try
代码段中出现错误后,会执行catch
代码段,并截获到错误信息 finally
不管是否有错误,都会执行
debugger :类似浏览器调试打断点
处理this
this指向-普通函数
普通函数的调用方式决定了 this 的值,即【谁调用 this 的值指向谁】
普通函数没有明确调用者时 this 值为 window,严格模式下没有调用者时 this 的值为 undefined
this指向-箭头函数
箭头函数中的 this 与普通函数完全不同,也不受调用方式的影响,事实上箭头函数中并不存在 this !
- 箭头函数会默认帮我们绑定外层 this 的值,所以在箭头函数中 this 的值和外层的 this 是一样的
- 箭头函数中的this引用的就是最近作用域中的this
- 向外层作用域中,一层一层查找this,直到有this的定义
改变this
call()
方法调用函数,同时指定被调用函数中 this 的值
fn.call(thisArg,arg1,arg2,...)
说明:
-
thisArg
:在 fn 函数运行时指定的this
值 -
arg1,arg2
:传递的其他参数 -
返回值就是函数的返回值,因为它就是调用函数
apply()
调用函数,同时指定被调用函数中 this 的值
fn.apply(thisArg,[argArray])
说明:
thisArg
:在fn函数运行时指定的 this 值argsArray
:传递的值,必须包含在数组里面- 返回值就是函数的返回值,因为它就是调用函数
- 因此
apply
主要跟数组有关系,比如使用Math.max()
求数组的最大值
bind()
不会调用函数。但是能改变函数内部this 指向
fn.bind(thisArg,arg1,arg2,...)
说明:
thisArg
:在 fn 函数运行时指定的this
值arg1,arg2
:传递的其他参数- 返回由指定的 this 值和初始化参数改造的 原函数拷贝 (新函数)
- 因此当我们只是想改变 this 指向,并且不想调用这个函数的时候,可以使用 bind,比如改变定时器内部的 this 指向.
总结:
-
区别
call
和apply
会调用函数, 并且改变函数内部 this 指向call
和apply
传递的参数不一样,call
传递参数aru1, aru2..
形式apply
必须数组形式[arg]
bind
不会调用函数, 可以改变函数内部 this 指向
-
主要应用场景
- call 调用函数并且可以传递参数
- apply 经常跟数组有关系. 比如借助于数学对象实现数组最大值最小值
- bind 不调用函数,但是还想改变this指向. 比如改变定时器内部的this指向.
节流和防抖
节流:
就是指连续触发事件但是在 n 秒中只执行一次函数,比如可以利用节流实现 1s之内 只能触发一次鼠标移动事件
防抖:
指触发事件后在 n 秒内函数只能执行一次,如果在 n 秒内又触发了事件,则会重新计算函数执行时间
使用场景
- 节流: 鼠标移动,页面尺寸发生变化,滚动条滚动等开销比较大的情况下
- 防抖: 搜索框输入,设定每次输入完毕n秒后发送请求,如果期间还有输入,则从新计算时间