JS进阶
作用域
作用域(scope)规定了变量能够被访问的“范围”,离开了这个“范围”变量便不能被访问,作用域分为全局作用域和局部作用域。
局部作用域
局部作用域分为函数作用域和块作用域
函数作用域
函数内部声明的变量,在函数外部无法被访问
函数的参数也是函数内部的局部变量
不同函数内部声明的变量无法互相访问
函数执行完毕后,函数内部的变量实际被清空了
块作用域
使用 {}
包裹的代码称为代码块,代码块内部声明的变量外部将【有可能】被访问。
当变量为var声明
{没声名就赋值的变量}
常量与变量本质的区别是【常量必须要有值且不允许被重新赋值】,常量值为对象时其属性和方法允许重新赋值。
总结
let
声明的变量会产生块作用域,var
不会产生块作用域const
声明的常量也会产生块作用域不同代码块之间的变量无法互相访问
推荐使用
let
或const
全局作用域
<script>
标签和 .js
文件的【最外层】就是所谓的全局作用域,在此声明的变量在函数内部也可以被访问。
为
window
对象动态添加的属性默认也是全局的,不推荐!函数中未使用任何关键字声明的变量为全局变量,不推荐!
尽可能少的声明全局变量,防止全局变量被污染
作用域链
作用域链:嵌套关系的作用域串联起来形成了作用域链。本质上是底层的变量查找机制
在函数被执行时,会优先查找当前函数作用域中查找变量,如果当前作用域查找不到则会依次逐级查找父级作用域直到全局作用域
子作用域能够访问父作用域,父级作用域无法访问子级作用域
垃圾回收机制
内存:分配->使用->回收
局部变量(函数调用)不用了就回收
内存泄漏(内存未释放或者说无法释放)
嵌套引用(函数里的对象互相引用)
闭包
闭包 = 内层函数 + 外层函数的变量
使用闭包能够访问函数作用域中的变量。从代码形式上看闭包是一个做为返回值的函数。
闭包的作用:封闭数据,实现数据私有,外部也可以访问函数内部的变量
闭包可能引起的问题:内存泄漏
闭包的写法 (统计函数的调用次数)
function outer() {
let count = 1
function fn() {
count++
console.log(`函数被调用${count}次`)
}
return fn
}
const re = outer()
变量提升
条件:先使用变量,再声明变量
let
声明的变量不存在变量提升,推荐使用let
var
声明声明的变量提升到 当前作用域的最前面,但是只提升声明,不提升赋值var
声明的变量被赋值函数,变量声明会提升到作用域最前面,但是没有赋值就调用函数,会报错变量提升出现在相同作用域当中(头部)
变量在声明之前即被访问,变量的值为
undefined
变量在未声明即被访问时会报语法错误(在同作用域中没有声明变量)
实际开发中推荐先声明再访问变量
函数提升
条件:函数在声明之前即可被调用
函数表达式不存在提升的现象(var 变量名=function(){})
函数表达式必须先声明和赋值,后调用
函数提升出现在相同作用域当中(头部)
只提升函数声明,不提升函数调用
函数参数
默认值
声明函数时为形参赋值即为参数的默认值
如果参数未自定义默认值时,参数的默认值为
undefined
调用函数时没有传入对应实参时,参数的默认值
undefined
被当做实参传入
动态参数
arguments
是函数内部内置的伪数组变量,它包含了调用函数时传入的所有实参。
只存在于函数中,通过for循环依次得到实参
箭头函数没有动态参数
function sum() {
let s = 0
for(let i = 0; i < arguments.length; i++) {
s += arguments[i]
}
console.log(s)
}
剩余参数
...
是语法符号,置于最末函数形参之前,用于获取多余的实参
借助 ...
获取的剩余实参,是个真数组
function sum(a,b,...arr) {
console.log(arr)//使用时不用加...
}
展开运算符(...)
作用:将一个数组展开
运用场景:求最大值和最小值(数组没有直接求最大,最小的函数),合并数组
Math.max(...arr)
Math,min(...arr)
const arr3=[...arr1,...arr2]
与剩余数组的区别
展开运算符的作用是数组展开
剩余参数 在函数内部使用(数组还没展开)
箭头函数
声明函数的简洁语法,它与普通函数并无本质的区别,差异性更多体现在语法格式上。
//无参箭头函数
const fn = () => {
console.log(123)
}
fn()
//有参箭头函数
const fn = (x) => {
console.log(x)
}
fn(1)
// 只有一个形参的时候,可以省略小括号
const fn = x => {
console.log(x)
}
fn(1)
//只有一行代码的时候,我们可以省略大括号
const fn = x => console.log(x)
fn(1)
//只有一行代码的时候,可以省略return
const fn = x => x + x
console.log(fn(1))
// 箭头函数可以直接返回一个对象,加括号
const fn = (uname) => ({ uname: uname })
console.log(fn('刘德华'))
箭头函数的this
是上一层的this指向
解构赋值
分为数组解构、对象解构两大类型。
数组解构
赋值运算符 =
左侧的 []
用于批量声明变量,右侧数组的单元值将被赋值给左侧的变量
// 普通的数组
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]
变量的顺序对应数组单元值的位置依次进行赋值操作
变量的数量大于单元值数量时,多余的变量将被赋值为
undefined
变量的数量小于单元值数量时,可以通过
...
剩余参数获取剩余单元值,但只能置于最末位允许初始化变量的默认值,且只有单元值为
undefined
时默认值才会生效(参数=默认值)
对象解构
赋值运算符 =
左侧的 {}
用于批量声明变量,右侧对象的属性值将被赋值给左侧的变量
// 普通对象
const user = {
name: '小明',
age: 18
};
// 批量声明变量 name age
// 同时将数组单元值 小明 18 依次赋值给变量 name age
const {name, age} = user
console.log(name) // 小明
console.log(age) // 18
const {name, age}= {
name: '小明',
age: 18
}
//数组对象解构
const [{}]=[{}]
对象属性的值将被赋值给与属性名相同的变量
属性名相同的变量能改名(属性名相同的变量名:新变量名)
对象中找不到与变量名一致的属性时变量值为 undefined
允许初始化变量的默认值,属性不存在或单元值为 undefined
时默认值才会生效
支持多维解构赋值
多维数组
const [a,[b,c]]=[1,[2,3]]
多维对象
function render({ data }) { } render(msg)
<body>
<script>
// 1. 这是后台传递过来的数据
const msg = {
"code": 200,
"msg": "获取新闻列表成功",
"data": [
{
"id": 1,
"title": "5G商用自己,三大运用商收入下降",
"count": 58
},
{
"id": 2,
"title": "国际媒体头条速览",
"count": 56
},
{
"id": 3,
"title": "乌克兰和俄罗斯持续冲突",
"count": 1669
},
]
}
// 需求1: 请将以上msg对象 采用对象解构的方式 只选出 data 方面后面使用渲染页面
// const { data } = msg
// console.log(data)
// 需求2: 上面msg是后台传递过来的数据,我们需要把data选出当做参数传递给 函数
// const { data } = msg
// msg 虽然很多属性,但是我们利用解构只要 data值
function render({ data }) {
// const { data } = arr
// 我们只要 data 数据
// 内部处理
console.log(data)
}
render(msg)
// 需求3, 为了防止msg里面的data名字混淆,要求渲染函数里面的数据名改为 myData
function render({ data: myData }) {
// 要求将 获取过来的 data数据 更名为 myData
// 内部处理
console.log(myData)
}
render(msg)
综合案例补充知识点
forEach遍历数组
forEach() 方法用于调用数组的每个元素,并将元素传递给回调函数
注意:
1.forEach 主要是遍历数组,没有返回值(
understand
)2.参数当前数组元素是必须要写的, 索引号可选。
const result = 数组名.forEach(function (item, index) {})
应用:遍历数组对象
数组名.forEach(function (item=> {
//对象解构
const {name, price,picture } = item
str += '<div>....${picture}</div>'
})
filter筛选数组
filter() 方法创建一个新的数组,新数组中的元素是通过检查(筛选)指定数组中符合条件的所有元素,并返回筛选之后元素的新数组
注意:
1.filter主要是筛选数组,返回新数组,如果没有符合条件的元素则返回空数组。不会影响原数组
2.参数是必须要写的, 索引号可选。
const newArr = arr.filter(item => item >= 20)
创建对象
利用对象字面量{}
利用new Object({})
利用构造函数
构造函数
语法:
1.命名习惯以大写字母开头
2.new 操作符执行
作用:快速创建多个类似的对象,初始化对象
// 定义函数
function Foo() {
console.log('通过 new 也能调用函数...');
}
// 调用函数
new Foo;
注意:
使用
new
关键字调用函数的行为被称为实例化实例化构造函数时没有参数时可以省略
()
构造函数的返回值即为新创建的对象
构造函数内部的
return
返回的值无效!不要写returnnew Object() new Date() 也是实例化构造函数
3.面向对象的构造函数实现的封装
——面向对象的特性:封装性、继承性、多态性
构造函数实例创建的对象彼此独立、互不影响(存在浪费内存的问题)
原型对象
构造函数通过原型分配的函数是所有对象所 共享的。
JavaScript 规定,每一个构造函数都有一个 prototype 属性,指向另一个对象,所以我们也称为原型对象
把那些不变的方法,直接定义在 prototype 对象上,这样所有对象的实例就可以共享这些方法。
构造函数和原型对象中的this 都指向 实例化的对象
构造函数中定义与原型对象中相同名称的方法,这时实例对象调用则是构造函中的方法。(当访问对象的属性或方法时,先在当前实例对象是查找,然后再去原型对象查找,并且原型对象被所有实例共享。)
constructor 属性
每个原型对象里面都有个constructor 属性,作用:该属性指向该原型对象的构造函数
使用场景:可以给原型对象采取对象形式赋值.但是这样就会覆盖构造函数原型对象原来的内容,在修改后的原型对象中,添加一个 constructor 指向原来的构造函数。
对象原型(__proto__
)
对象都会有一个属性 proto 指向构造函数的 prototype 原型对象,之所以我们对象可以使用构造函数 prototype
原型对象的属性和方法,就是因为对象有 proto 原型的存在。
注意:
proto 是JS非标准属性
[[prototype]]和proto意义相同
用来表明当前实例对象指向哪个原型对象prototype
proto对象原型里面也有一个 constructor属性,指向创建该实例对象的构造函数
三者关系
原型继承
继承
子类的原型 = new 父类
Woman.prototype = new Person()
指回原来的构造函数
(子.prototype.constructor = 子)
调用
const 变量名= new 子()
子扩张
构造函数 new 出来的对象 结构一样,但是对象不一样——new继承
子.prototype.baby = function () {}
原型链
①当访问一个对象的属性(包括方法)时,首先查找这个对象自身有没有该属性。
② 如果没有就查找它的原型(也就是 proto指向的 prototype 原型对象)
③ 如果还没有就查找原型对象的原型(Object的原型对象)
④ 依此类推一直找到 Object 为止(null)
⑤ proto对象原型的意义就在于为对象成员查找机制提供一个方向,或者说一条路线
⑥ 可以使用 instanceof 运算符用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上
应用
实例成员
通过构造函数创建的对象称为实例对象,实例对象中的属性和方法称为实例成员。
特点:
构造函数内部
this
实际上就是实例对象,为其动态添加的属性和方法即为实例成员为构造函数传入参数,动态创建结构相同但值不同的对象
调用:实例成员.属性||方法()
// 构造函数
function Person() {
// 构造函数内部的 this 就是实例对象
// 实例对象中动态添加属性
this.name = '小明'
// 实例对象动态添加方法
this.sayHi = function () {
console.log('大家好~')
}
}
// 实例化,p1 是实例对象
// p1 实际就是 构造函数内部的 this
const p1 = new Person()
console.log(p1)
console.log(p1.name) // 访问实例属性
p1.sayHi() // 调用实例方法
注:构造函数创建的实例对象彼此独立互不影响。
静态成员
构造函数的属性和方法被称为静态成员。
在 JavaScript 中底层函数本质上也是对象类型,因此允许直接为函数动态添加属性或方法。
// 构造函数
function Person(name, age) {
// 省略实例成员
}
// 静态属性
Person.eyes = 2
Person.arms = 2
// 静态方法
Person.walk = function () {
console.log('人都会走路...')
// this 指向 Person
console.log(this.eyes)
}
特点:
静态成员指的是添加到构造函数本身的属性和方法
静态成员方法中的
this
指向构造函数本身
内置构造函数
字符串、数值、布尔、数组、普通对象也都有专门的构造函数,用于创建对应类型的数据。
字符串、数值、布尔、undefined、null 也被称为简单类型或基础类型,对象也被称为引用类型。
Object
const user = new Object({name: '小明', age: 15})
等于 const user = { name: '小明', age: 15 }
注意:
推荐使用字面量方式声明对象,而不是
Object
构造函数Object.assign
静态方法创建新的对象Object.keys
静态方法获取对象中所有属性Object.values
表态方法获取对象中所有属性值
Array
Array
是内置的构造函数,用于创建数组。
let arr = new Array(5, 7, 8);
等于let arr = [5,7,8]
实例方法
forEach
用于遍历数组,不返回数组,经常用于查找遍历数组元素filter
过滤数组单元值,生成新数组map
迭代原数组,生成新数组reduse
累计器,返回累计处理的结果,经常用于求和join
数组元素拼接为字符串,返回字符串(重点)find
查找元素, 返回符合测试条件的第一个数组元素值,如果没有符合条件的则返回 undefined(重点)every
检测数组所有元素是否都符合指定条件,如果所有元素都通过检测返回 true,否则返回 false(重点)当是空数组时,返回true
from 转换为真数组
some
检测数组中的元素是否满足指定条件 如果数组中有元素满足条件返回 true,否则返回 falseconcat
合并两个数组,返回生成新数组sort
对原数组单元值排序splice
删除或替换原数组单元reverse
反转数组findIndex
查找元素的索引值
String
String
是内置的构造函数,用于创建字符串。
实例属性
length
用来获取字符串的度长(重点)实例方法
split('分隔符')
用来将字符串拆分成数组(重点)实例方法
substring(需要截取的第一个字符的索引[,结束的索引号])
用于字符串截取(重点)实例方法
startsWith(检测字符串[, 检测位置索引号])
检测是否以某字符开头(重点)实例方法
includes(搜索的字符串[, 检测位置索引号])
判断一个字符串是否包含在另一个字符串中,根据情况返回 true 或 false(重点)实例方法
toUpperCase
用于将字母转换成大写实例方法
toLowerCase
用于将就转换成小写实例方法
indexOf
检测是否包含某字符实例方法
endsWith
检测是否以某字符结尾实例方法
replace
用于替换字符串,支持正则匹配实例方法
match
用于查找字符串,支持正则匹配
注:String 也可以当做普通函数使用,这时它的作用是强制转换成字符串数据类型。
Number
Number
是内置的构造函数,用于创建数值。
// 使用构造函数创建数值
let x = new Number('10')
let y = new Number(5)
// 字面量创建数值
let z = 20
实例方法 toFixed
用于设置保留小数位的长度->toFixed(数字n)=保留n位小数
深浅拷贝
浅拷贝
首先浅拷贝和深拷贝只针对引用类型
浅拷贝:拷贝的是地址
常见方法:
拷贝对象:Object.assgin() / 展开运算符 {...obj} 拷贝对象
拷贝数组:Array.prototype.concat() 或者 [...arr]
深拷贝
首先浅拷贝和深拷贝只针对引用类型
深拷贝:拷贝的是对象,不是地址
递归实现深拷贝
函数递归:
如果一个函数在内部可以调用其本身,那么这个函数就是递归函数
简单理解:函数内部自己调用自己, 这个函数就是递归函数
递归函数的作用和循环效果类似
由于递归很容易发生“栈溢出”错误(stack overflow),所以必须要加退出条件 return
常见方法:
通过递归实现深拷贝
递归调用
处理数组的问题 一定先写数组 在写 对象 不能颠倒
const obj = {
uname: 'pink',
age: 18,
hobby: ['乒乓球', '足球'],
family: {
baby: '小pink'
}
}
const o = {}
// 拷贝函数
function deepCopy(newObj, oldObj) {
debugger
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, obj) // 函数调用 两个参数 o 新对象 obj 旧对象
lodash/cloneDeep
const 新 = _.cloneDeep(旧)
通过JSON.stringify()实现
把对象转换为 JSON 字符串
const o = JSON.parse(JSON.stringify(obj))
异常处理
throw
throw 抛出异常信息,程序也会终止执行
throw 后面跟的是错误提示信息
Error 对象配合 throw 使用,能够设置更详细的错误信息
function counter(x, y) {
if(!x || !y) {
// throw '参数不能为空!';
throw new Error('参数不能为空!')
}
return x + y
}
counter()
try ... catch
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指向
使用
call
方法调用函数,同时指定函数中this
的值// 调用 counter 函数,并传入参数
let result = counter.call(null, 5, 10);
console.log(result);
call
方法能够在调用函数的同时指定this
的值使用
call
方法调用函数时,第1个参数为this
指定的值call
方法的其余参数会依次自动传入函数做为函数的参数
使用
apply
方法调用函数,同时指定函数中this
的值调用 counter 函数,并传入参数
let result = counter.apply(null, [5, 10])
console.log(result)
apply
方法能够在调用函数的同时指定this
的值使用
apply
方法调用函数时,第1个参数为this
指定的值apply
方法第2个参数为数组,数组的单元值依次自动传入函数做为函数的参数应用场景:求数组最大值
bind
方法并不会调用函数,而是创建一个指定了this
值的新函数调用 bind 指定 this 的值
let sayHello = sayHi.bind(user);
调用使用 bind 创建的新函数
sayHello()
防抖节流
防抖(debounce)所谓防抖,就是指触发事件后在 n 秒内函数只能执行一次,如果在 n 秒内又触发了事件,则会重新计算函数执行时间
节流(throttle)所谓节流,就是指连续触发事件但是在 n 秒中只执行一次函数