JS进阶
作用域
局部作用域和全局作用域
局部作用域
局部作用域:函数作用域(函数内部) 和 代码块作用域 { }
局部作用域声明的变量外部不能用
全局作用域
script中间
.js 文件之间
作用域链
作用域链本质上是最底层的变量查找机制
总结:
- 嵌套关系的作用域链串联起来形成了作用域链
- 相同作用域链中按着从小到大的规则查找变量
- 子作用域链能够访问父作用域,父级作用域无法访问子作用域
像链子一样从小往大依次查找
垃圾回收机制
全局变量一般不会回收,页面关闭时回收
一般情况下局部变量的值,不用了会被自动回收掉
引用计数法
问题就是,相互引用时引用次数永远不是0.永远回收不了,造成回收泄露
标记清除法
从根部定期扫描对象进行寻找,如在继续使用则不回收
不再继续使用即刻进行回收
闭包
闭包 = 内层函数 + 外层函数的变量
作用:封闭数据,提供数据,外部可以访问函数内部的变量
应用:实现数据的私有
引起的问题:内存泄漏
<script>
function count(){
let i = 0
function fu(){
i++
console.log(`调用了${i}次函数`);
}
return fu
}
const fun = count()
</script>
函数进阶
函数参数
arguments法
当不知道有几个形参时,采用arguments动态参数
注意:arguments只存在于函数之间,并且arguments时伪数组
<script>
function getSum(){
let s = 0
for(let i = 0; i < arguments.length;i++){
s += arguments[i]
}
console.log(s);
}
getSum(2,3,4,5,6)
</script>
剩余参数法
arr代表的是真数组,多用剩余函数,写在函数内部
a,b,...arr 代表传过来的实参已经给a,b值了,arr代表的是剩余参数的数组
<script>
function getsum(a,b,...arr / ...arr){
let s = 0
for(let i = 0; i < arr.length;i++){
s += arr[i]
}
console.log(s);
}
getsum(2,3,4,5,6) //15
</script>
展开运算符
把数组展开,多用于求数组最大值和最小值 和合并数组
格式和剩余函数一样。但是区别是,剩余函数用于函数内部,展开运算符主要在数组中展开
<script>
const arr1 = [1,2,3]
const arr2 = [4,5,6]
console.log(Math.max(...arr1));
console.log(Math.min(...arr1));
const arr3 = [...arr1,...arr2]
console.log(arr3);
</script>
箭头函数
语法
<script>
1.箭头函数基本语法
const fn = () =>{
console.log(111);
}
fn()
2.当只有一个形参时可以省略小括号
const fn = x =>{
console.log(x);
}
fn(1)
3.函数体只有一句话时可以省略大括号
const fn = x => console.log(x)
fn(1)
4.函数有return返回值时可省略return
const fn = x => x+x
console.log(fn(1));
5.可以直接利用字面量返回一个对象
const fn = (uname) => ({uname:uname})
console.log(fn('liu'));
</script>
注意:
- 箭头函数没有arguments动态参数。但是有剩余函数...argus
- 箭头函数没有自己的this值,箭头函数中所使用的this都是来自函数作用域链,它的取值遵循普通普通变量一样的规则,在函数作用域链中一层一层往上找。
this指向问题
?????
解构赋值
数组解构
把数组的批量单元值赋给一系列的变量
const [a,b,c] = [1,2,3]
console.log(a);
情况:
支持多维数组结构
变量数值大于单元值数量,多于变量值赋值为undifined
变量数值小于单元值数量,不进行赋值,直接忽略
或者用剩余函数来解决
js中需要加封号的两种情况
立即执行函数
(function t(){})(); //必须加封号
(function t(){})()
数组结构,数组开头时必须加封号
let a = 1
let b = 2; //必须加封号
[b,a] = [a,b]
console.log(a);
console.log(b);
对象解构
把对象的一系列属性和方法赋值给变量
但是注意:变量名称必须和对象属性或者方法名称一样
const {uname,age} = {uname:'pink',age:18}
console.log(uname);
console.log(age);
对象解析的变量名可以改名
const {uname:name,age} = {uname:'pink',age:18}
console.log(name);
console.log(age);
数组对象解构
const pig = [{
uname:'佩奇',
age:5
}]
const [{uname,age}] = pig
多级对象解构
const pig = {
uname:'佩奇',
family:{
father:'bb',
mother:'mm'
}
}
const {uname,family:{father,mother}} = pig
console.log(uname);
console.log(father);
console.log(mother);
数组常见方法
forEach遍历数组
forEach((ele,index){ 函数体 })
用于遍历数组,但是与map唯一的区别就是没有返回值
数组元素是必须写的,但是索引号可写可不写
适用于遍历数组对象
map遍历数组修改数据
map用于修改数组的数据,并且返回一个新的数组,与foreach一样使用,但是foreach不返回值
const newArr = map(function(ele,index)){
}
<script>
const arr = ['pink','red','green']
//arr.map(function(ele数组元素 , index数组索引值){}
const newarr = arr.map(function(ele , index){
return ele + '颜色'
})
console.log(newarr);
</script>
filter数组筛选
<script>
// const arr = [1,2,3]
// const newarr = arr.filter(function(item,index){
// return item >= 2
// })
// console.log(newarr);
const arr = [1,2,3]
const newarr = arr.filter(item => item >= 2)
console.log(newarr);
</script>
数组reduce方法求和
注意:有返回值,必须return
通过循环重新赋值进行求和
<script>
const arr = [1,5,9]
//无初始值的情况 起始值 下一个值
// const total = arr.reduce(function(prev,current){
// return prev + current
// })
// console.log(total);
//有初始值的情况
// const total = arr.reduce(function(prev,current){
// return prev + current
// },10) 要加初始值,如果没有加0即可
// console.log(total);
//箭头函数表示
const total = arr.reduce((prev,current) => prev + current,10)
console.log(total);
</script>
数组find以及some
every 判断数组中满不满足条件,若有全部满足,返回true;有一个不满足,返回false
some 判断数组中满不满足条件,若有一个满足,返回true,若全部不满足,返回false
<script>
const arr = [1,2,3]
//every 判断数组中满不满足条件,若有全部满足,返回true;有一个不满足,返回false
// const flag = arr.every(function(item){
// return item >= 2
// })
// console.log(flag); //false
//箭头函数
// const flag = arr.every(item => item >= 2)
// console.log(flag); //false
//some 判断数组中满不满足条件,若有一个满足,返回true
const flag = arr.some(function(item){
return item >= 2
})
console.log(flag); //true
</script>
from把伪数组转换为真数组
格式:Array.from(伪数组)
const lis = document.querySelectorAll('ul li')
const liss = Array.from(lis)
console.log(liss);
join把数组转换为一个字符串、
console.log(newarr.join()); //小括号里面没有东西,则使用逗号分割 pink颜色,red颜色,green颜色
console.log(newarr.join('')); //里面是空字符串,则连在一起, pink颜色red颜色green颜色
console.log(newarr.join('|')); //里面是什么字符就用什么字符分割 pink颜色|red颜色|green颜色
深入函数
创建对象(三种)
构造函数
-
用来初始化对象,
-
使用场景:用于快速创建多个对象
-
约定:函数的命名以大写字母开头
创建函数时必须要new
- new关键字调用函数的行为称为实例化
- 构造函数内部不需要写return有默认的返回值
- 好用。但是有内存浪费问题
function Pig(uname,age){
this.name = uname
this.age = age
}
const Peiqi = new Pig('佩奇','5')
const Qiao = new Pig('乔治','5')
const Ma = new Pig('猪妈妈','5')
实例成员和静态成员
实例成员
通过构造函数创建的对象是实例对象
实例对象的属性和方法成为实例成员(实例属性和实例方法)
构造函数创建的实例对象彼此独立互不影响
function Pig(uname){
this.name = uname
}
const peiqi = new Pig('佩奇')
//把属性加到对象身上就是实例成员和方法
peiqi.age = '4'
// peiqi.say = () => {
// console.log('hi');
// }
peiqi.say = function(){
console.log('hi');
}
console.log(peiqi);
静态成员
构造函数的属性和方法
function Pig(uname){
this.name = uname
}
Pig.eye = 3
console.log(Pig.eye);
内置构造函数
Object
作用:Object.keys(对象)静态方法获取对象中所有属性(键)
Object.values(对象)静态方法获取对象中所有值
注意:返回的是一个数组
<script>
const o = {
uname:'pink',
age:'13'
}
console.log(Object.keys(o)); // ['uname', 'age']
console.log(Object.values(o)); // ['pink', '13']
</script>
Object.assign(被拷贝的对象,拷贝的对象)静态方法可以进行拷贝数值
使用:常用于添加属性
const o = {
uname:'pink',
age:'13'
}
console.log(Object.keys(o)); // ['uname', 'age']
console.log(Object.values(o)); // ['pink', '13']
const oo = {}
Object.assign(oo,o)
console.log(oo); //{uname: 'pink', age: '13'}
Object.assign(oo,{gender:'女'})
console.log(oo); //{uname: 'pink', age: '13', gender: '女'} //常用于添加属性
字符串常见方法
split(‘分隔符’) 用于把字符串转换为数组
<script>
//split() 用于把字符串转换为数组。正好与join()方法相反
//split括号里面是字符串之间分隔符
const arr = 'pink,red'
const str = arr.split(',')
console.log(str); //['pink', 'red']
const arr1 = '2022-07-30'
const str1 = arr1.split('-')
console.log(str1); //['2022', '07', '30']
</script>
substing(起始位置,结束位置)用于分割字符串
//substing(起始位置,结束位置)用于分割字符串
//但是注意:结束位置输出内容不包括该索引号所对应的字符
const a = 'pinkred'
console.log(a.substring(0,3)); //pin
console.log(a.substring(0,4)); //pink
startsWith('字符') 用于判断是否是以该字符开头
//startsWith('字符') 用于判断是否是以该字符开头
//startsWith('字符',数字) 用于判断是否是以该数字位置的字符开头
//endsWith('字符') 用于判断是否是以该字符结尾
const b = 'asdfsghjk'
console.log(b.startsWith('a')); //true
console.log(b.startsWith('b')); //false
console.log(b.startsWith('f',3)); //true
includes('字符') 用于判断是否包含该字符
// includes('字符') 用于判断是否包含该字符。但是切记区分大小写
const c = 'asdfFhj'
console.log(c.includes('s')); //true
NUmber保留小数方法
toFixed() 括号里面保留几位小数。四舍五入
const num = 10.234
console.log(num.toFixed());
原型对象
构造函数里面的属性可以直接用,但是里面的方法不能共享,每创建一个对象就必须重新开辟构造函数中的方法的空间,会造成资源浪费,为了防止这种资源的浪费,我们把属性定义在构造函数内部,方法用原型对象 构造函数的名字.prototype.方法名 = function(){
console.log(11);
}来定义,所有new出来的 对象都可以使用,共享空间,不会造成浪费
构造函数都会自动生成原型对象
<script>
function Star(name,age){
this.name = name,
this.age = age
// this.sing = function(){
// console.log(11);
// }
}
Star.prototype.sing = function(){
console.log(11);
}
const ldf = new Star('ldf','12')
const pink = new Star('pink','14')
// console.log(ldf.sing === pink.sing); //false
console.log(ldf.sing === pink.sing); //true
</script>
注意
原型的作用:共享方法。
把不变的方法定义在prototype中
构造函数和原型里面的this都指向实例化的对象
constructor属性
属于prototype的属性,可以指回原函数
使用场景:
当构造函数里面的方法过多时,直接给Star.prototype进行赋值,但是赋值之后就丢失了原本的constractor属性,找不到是哪个构造函数(相当于找不到 父亲)所以在给Star.prototype进行赋值后先要使constructor指向原来的构造函数 例如:constructor : Star,
<script>
function Star(name){
this.name = name
}
//正常应该这样写。但是当构造函数里面的方法过多时这样写比较繁琐
// Star.prototype.sing = function(){
// console.log(11);
// }
// Star.prototype.dance = function(){
// console.log(22);
// }
//所以直接给Star.prototype进行赋值,但是赋值之后就丢失了原本的constractor属性,找不到是哪个构造函数(相当于找不到 父亲)所以在给Star.prototype进行赋值后先要使constractor指向原来的构造函数
Star.prototype = {
constractor : Star, //重点
sing: function(){
console.log(11);
},
dance: function(){
console.log(22);
}
}
console.log(Star.prototype);
</script>
对象原型
每new一次实例化对象,都会生成对象原型 proto
对象原型指向原型对象
原型对象和对象原型都用constructor指回构造函数
原型继承
分三步
- 提取共有的部分
- 利用原型对象来继承
- 继承之后会把原有的constructor属性覆盖,重新把constructor属性指回
<script>
function Person(){
this.eyes = 2
this.head = 1
}
function Woman(){
}
function Man(){
}
Woman.prototype.baby = function(){
console.log(11);
}
//子类的原型 = new 父类()
Woman.prototype = new Person() //先利用原型对象来继承
Woman.prototype.constructor = Woman //继承之后会把原有的constructor属性覆盖,重新把constructor属性指回
const a = new Woman()
console.log(a);
Man.prototype = new Person()
Man.prototype.constructor = Man
const b = new Man()
console.log(b);
</script>
注意:
只要有对象就有proto
只要有原型对象就有constructor
instanceof 用于检测该对象是否在原型链上 对象 instanceof 原型链
深浅拷贝
浅拷贝
拷贝只能用于引用数据类型
浅拷贝只能拷贝一层的对象,不能拷贝对象里面包含的对象
对象拷贝 Object.assign(给谁拷贝,拷贝的对象)
const o = {...obj}
数组拷贝 Array.prototype.concat()
[...arr]
<script>
const obj = {
age:11,
name:'dddd'
}
// 对象拷贝1.
const o = {}
Object.assign(o,obj)
console.log(o);
// 2.对象拷贝2
const o1 = {...obj}
console.log(o1);
const arr = [1,2,3,4]
const arr2 = []
//数组拷贝1
const arr3 = Array.prototype.concat(arr,arr2)
console.log(arr3); //[1,2,3,4]
//数组拷贝2
const arr4 = [...arr]
console.log(arr4);
</script>
深拷贝
利用递归函数实现深拷贝
<script>
//用递归函数实现setTimeout模仿setInterval
function getTime(){
document.querySelector('div').innerHTML = new Date().toLocaleString()
setTimeout(getTime,1000)
}
getTime()
</script>
深拷贝步骤:
- 先写深拷贝函数
- 判断数组,递归函数实现
- 判断对象,递归函数实现
<script>
const obj = {
uname:'pink',
age:20,
hobby:['蓝','足'],
family:{
baby:'oldpink'
}
}
const o = {}
function deepCopy(newObj,oldObj){
for(let k in oldObj){
//处理数组 k是属性名
//先判断Array
if(oldObj[k] instanceof Array){ //必须先判断数组然后判断对象,不能颠倒
newObj[k] = []
deepCopy(newObj[k],oldObj[k]) //此时的k指的是索引值
}else if(oldObj[k] instanceof Object){ //再判断Object,因为数组也属于Object
newObj[k] = {}
deepCopy(newObj[k],oldObj[k])
} else{
newObj[k] = oldObj[k]
}
}
}
deepCopy(o,obj)
console.log(o);
o.hobby = ['www']
o.family.baby = 'newpink'
console.log(obj);
</script>
利用lodash在线链接
用 _.cloneDeep(obj)方法
<script src="https://cdn.bootcdn.net/ajax/libs/lodash.js/4.17.20/lodash.min.js"></script>
<script>
const obj = {
uname:'pink',
age:20,
hobby:['蓝','足'],
family:{
baby:'oldpink'
}
}
const o = _.cloneDeep(obj)
console.log(o);
</script>
利用JSON深拷贝
const o = JSON.parse(JSON.stringify(obj))
console.log(o);
异常处理
throw抛出异常
throw抛出异常会中止程序,经常和 new Error()搭配使用
<script>
function fn(x,y){
if(!x || !y){
throw new Error('没有参数传递')
}
return x + y
}
console.log(fn());
</script>
try catch finally 捕获异常
this指向
普通函数
- 通常情况下(非严格模式,若没使用 'use strict'),没找到直接调用者,则this指的是 window ;
- 严格模式,没有直接调用者的函数中的this是 undefined;
- this代表它的直接调用者(js中的this是执行上下文), 例如 obj.fun ,fun中的this就是obj;
- 使用call,apply,bind(ES5新增)绑定的,this指的是绑定的对象;
箭头函数
- 箭头函数无自己的this, 它的this是继承而来; 默认指向在定义它时所处的对象(宿主对象),此处指父级作用域,而不是执行时的对象, 定义它的时候,可能环境是window; 箭头函数可以让我们在 setTimeout ,setInterval中方便的使用this;
- 在箭头函数中this指向的固定化,并非箭头函数内部有绑定this的机制,而是箭头函数无自己的this,导致内部的this就是外层代码块的this。
改变this指向
call,apply,bind方法最牛之处在于,call,apply,bind指向那个对象后,相当于那个对象可以用当前的方法,比如[].slice.apply(A),其实是让A数组使用slice方法
call方法
<script>
function fn(x,y){
console.log(this);
console.log(x+y);
}
fn.call(Object,1,2)
</script>
apply方法
blind方法
注意:blind不调用函数,只是改变this指向,返回值是一个函数
let xiaowang = {
name:'小王',
gender:'男',
age:22,
say:function(school,grade){
console.log(this); //{name: '小红', gender: '女', age: 22}
console.log('姓名:' + this.name + ' 性别:' + this.gender + ' 年龄: ' + this.age + ' 在: ' + school + ' 上:' + grade)
}
}
let xiaohong = {
name:'小红',
gender:'女',
age:22
}
xiaowang.say.apply(xiaohong,["实验小学","六年级"]) //姓名:小红 性别:女 年龄: 22 在: 实验小学 上:六年级
xiaowang.say.call(xiaohong,"实验小学","六年级") //apply和call的区别,apply传参是数组的形式,是内包含,也会一一对应
var ff=xiaowang.say.bind(xiaohong,"实验小学","六年级")
ff() // bind返回的是一个新函数,我们还需要在调用一次
总结
重点:箭头函数,不能改变this指向,只有普通function函数,能改变this指向
改变this指向的方法
1, call()方法 语法: 函数.call(参数1,其他参数....可以是多个或者没有 ) 作用: 调用并且执行函数,同时,将函数的this指向,定义为指定的内容(参数1) 参数1,是改变的this的指向 其他参数,是原始函数的实参,原始函数有几个形参,此时就要对应的输入几个实参,没有形参,就没有实参
2, apply()方法 语法: 函数.apply(参数1,参数2) 只有两个参数 参数1:改变的this的指向内容 参数2:原始函数的实参,必须是一个数组的形式,将实参定义成数组的单元 其他用法和作用于 .call是相同的
总结: call方法与apply方法,作用,效果,都是完全一致的 只是对于原始函数的参数赋值方法,不同 call方法是通过其他多个参数来实现 apply方法是通过一个数组参数,来实现 两个方法没有本质的区别,爱用哪个用那个
3, bind()方法 语法: const 变量 = 函数.bind(参数1); 不是立即执行函数(下一篇博客有介绍 立即执行函数) 生成一个新的函数,这个新的函数是改变this指向之后的新的函数 参数1,定义的要改变的的this指向 其他参数,一般不定义,是使用函数原有的形参
总结: call apply 都是立即执行函数 参数1,都是改变的this指向 其他参数,是原始函数的形参(可以有,也可以没有) bind 不是立即执行函数,是生成一个新的函数 参数1,是改变的this指向 就使用原始函数的形参
const obj1 = {
name:'张三',
age:18,
sex:'男',
}
const obj2 = {
name:'李四',
fun2 : function(){
console.log(this);
}
}
// 对象中的函数,this指向的是这个对象,obj2
obj2.fun2();
// 改变this指向,指向的是obj1这个对象
// 代用,并且执行fun2这个函数,同时将fun2的this指向,从原始的obj2,改变为obj1
obj2.fun2.call(obj1);
// 带有参数的函数,this指向的改变
// 定义的带有参数的普通函数
function fun3(name,age,sex){
console.log(name,age,sex,this);
}
// 执行时,输出实参,此时this指向是window
fun3('张三',18,'男');
// 改变this指向 , call方法
fun3.call(obj1,'李四',20,'女');
// 改变this指向 , apply方法
fun3.apply(obj1 , [ '王五' , 20 , '不知道' ])
// bind方法,不是立即执行函数,而是定义生成一个新的函数
// 新生成的函数,this指向是参数1
// 新生成的函数,形参是原始函数fun3的形参
const fun4 = fun3.bind(obj1); //注意
fun4('王二麻子' , 100 , '不详');
防抖
有的操作是高频触发的,但是其实触发一次就好了,比如我们短时间内多次缩放页面,那么我们不应该每次缩放都去执行操作,应该只做一次就好。再比如说监听输入框的输入,不应该每次都去触发监听,应该是用户完成一段输入后在进行触发。
lodash里面的debounce函数,直接
_.debounce(函数体,毫秒值)
<div class="box"></div>
<script src="https://cdn.bootcdn.net/ajax/libs/lodash.js/4.17.20/lodash.min.js"></script>
<script>
const box = document.querySelector('.box')
let i = 1
function mouseMove(){
box.innerHTML = i++
console.log(i);
}
// box.addEventListener('mousemove',mouseMove)
box.addEventListener('mousemove',_.debounce(mouseMove,500))
</script>
防抖函数实现
思路:利用setTimeout定时器来实现
- 声明定时器变量
- 每次鼠标移动的时候都要先判断是否存在定时器,如果存在,先关掉定时器
- 如果没有,则开启定时器,存储在定时器变量里面
- 定时器里面有函数调用
<div class="box"></div>
<script>
const box = document.querySelector('.box')
let i = 1
//这是防抖函数执行的语句
function mouseMove(){
box.innerHTML = i++
// console.log(i);
}
//这是一个封装好的防抖函数
function debounce(fn,t){
let timer
return function(){ //注意此处的写法,要用return返回一个函数才能达到一直调用的效果
if(timer) clearTimeout(timer) //如果存在定时器,先关掉定时器
timer = setTimeout(function(){
fn()
},t)
}
}
//新的函数写法,
box.addEventListener('mousemove',debounce(mouseMove,500))
</script>
节流
在500ms内,不管触发多少次事件,都只执行一次
使用场景:在高频鼠标移动或者resize变化时
节流就是减少流量,将频繁触发的事件减少,并每隔一段时间执行。即,控制事件触发的频率
用lodash节流函数throttle来做
box.addEventListener('mousemove',_.throttle(mouseMove,3000))
节流函数
<div class="box"></div>
<script>
const box = document.querySelector('.box')
let i = 1
function mouseMove(){
box.innerHTML = i++
console.log(i);
}
function throttle(fn,t){
let timer = null
return function(){
if(!timer){
timer = setTimeout(function(){
fn()
timer = null //注意:这不能使用clearTimeout,因为在定时器内部关闭不了定时器,要想关闭定时器只 能赋值为null
},t)
}
}
}
box.addEventListener('mousemove',throttle(mouseMove,3000))
</script>
总结
防抖和节流相同点:
- 防抖和节流都是为了阻止操作高频触发,从而浪费性能。
防抖和节流区别:
- 防抖是触发高频事件后n秒内函数只会执行最后一次,如果n秒内高频事件再次被触发,则重新计算时间。适用于可以多次触发但触发只生效最后一次的场景。
- 节流是高频事件触发,但在n秒内只会执行一次,如果n秒内触发多次函数,只有一次生效,节流会稀释函数的执行频率。