1. 作用域、解构、箭头函数

1.1 作用域

作用域规定了变量能够访问的范围。
局部作用域
和块作用域

函数作用域
函数内部声明的变量只能在函数内部被访问,外部无法直接访问。

块作用域(函数 if for 都是一种块作用域)
用{}包裹的代码称为代码块,代码块内部声明的变量外部有可能无法访问。(let const 会产生块作用域,var不会产生块作用域)


全局作用域
script标签内或者js文件都是全局作用域,挂载在window对象下面。


作用域链
作用域链本质是底层的变量查找机制。
在函数被执行时,会优先查找当前函数作用域中查找变了。
找不到了逐级查找父级作用域。
子作用域能够访问父作用域。父作用域无法访问子作用域。


js垃圾回收机制GC

JS的生命周期:
1. 内存分配:声明变量、函数、对象的时候,系统自动分配内存。
2. 内存使用:即读写内存,也就是使用变量、函数等。
3. 内存回收:使用完毕,由垃圾回收器自动回收不再使用的内存。
*1 全局变量不会回收,关闭页面的时候才回收
*2 一般情况下局部变量的值不用了会被自动回收。
*3 内存泄漏---- 程序中分配的内存由于某种原因程序未释放或无法释放叫做内存泄漏。

算法说明

栈:由操作系统自动分配释放函数的参数值、局部变量等,基本数据类型存放在栈中
堆:要么程序员手动释放,要么垃圾回收机制回收,复杂类型数据存放在堆中

常见的垃圾回收机制

  1. 引用计算法
(过时了) 
就是看一个复杂类型数据是否有指向他的引用(因为垃圾回收机制回收的是堆中的数据,所以就是针对的是复杂类型的数据),如果没有就回收对象。
算法:
1 跟踪记录被引用的次数
2 如果被引用了一次,引用次数++,减少引用,引用次数--。
3 如果引用次数为0,释放内存。
缺点: 嵌套引用,指的是两个对象相互引用,尽管他们都不再使用了,垃圾回收机制也不会进行回收,导致内存泄漏 
  1. 标记清除法
无法达到的对象
就是从JS中的全局对象定时扫描内存中的对象,凡是能从JS全局到达的对象,就是还需要使用的。达到不了的对象就会被标记为不再使用,可以进行回收。

闭包
闭包= 内层函数+外层函数的变量。
内层函数用到了外层函数中的变量。

闭包

function outer(){
    const a = 11;
    inner();
    function inner(){
        console.log(a);
    }
}
outer()

在这里插入图片描述
普通函数

function outer(){
    const a= 11;
    inner();
    function inner(){
        console.log(11);
    }
}
outer()

在这里插入图片描述
闭包的作用

1 封闭数据,数据私有,外部也可以访问函数内部的变量**(因为有作用域的影响,所以一般情况下let const生命的变量不能访问函数内部的变量)**

闭包的常用方法(外部获取函数内部的变量)

就是让函数outer返回值是一个函数inner,函数inner

function outer(){
    const a= 11;
    function inner(){
        return a;
    }
    return  inner;
}
// outer()===inner === function inner(){ return a;}
const fun = outer();
const count = fun(); // 11

闭包的应用(实现数据的私有)

目标:统计函数被调用的次数

// 普通写法  使用全局变量
let count = 0;
function js(){
	console.log(`被访问了${++count}`)
}

/*
缺点“:全局变量,容易被修改
*/


// 闭包写法
function js(){
	let count = 0;
	return function(){
		console.log(`被访问了${++count}`)
	}
}
const fun = js();
/*
根据标记清除法
因为fun是绑定在window上 所以是全局变量
fun ---> js() ----> js内部分会的对象 ----> count
所以count是被一直引用的,所以不会被垃圾回收机制回收,信息可以保留下来。

缺点:内存泄露
*/

变量提升
指的就是变量还没有声明之前就能使用,(由var声明的变量。
机制:在代码运行之前,会把所有var声明的变量提到当前作用域的最前面。(全局变量中,提升到全局最前面,函数中只提升到函数运行的最前面)(只提升声明,不提升赋值

        function cc(){
           console.log(num);//undefined
           var num = 10;
       }
       /*变量未声明之前访问报错*/
       // console.log(num); //num is not defined
       cc();
       // console.log(num); //num is not defined

1.2 函数的进阶

函数的默认值,动态参数,剩余参数

函数提升

函数的声明会提升到当前作用域的最前面,但是函数表达式不会。

//函数声明 
cc();
function cc(){
    console.log(num);
    var num = 10;
}
/* 会先function cc(){}之后,再进行cc()、 所以cc()写在声明的前后都可以*/

// 函数表达式
var fun = function cc(){}
fun()
/*fun()只能写在var fun = function cc(){}之后,因为var有变量提升,但是只提升声明不提升赋值*/

函数参数

动态参数arguments 函数内部内置的伪数组变量,只存在函数中,并且箭头函数中没有动态参数

let sum = 0;
function add(){
	for(let i = 0; i<arguments.length; i++){
		sum += arguments[i];
	}
}
add(1,2,3);

剩余参数 是一个真实数组,

let sum = 0;
function add(...arr){
	arr.forEach(item => sum+=item);
}
add(1,2,3);

伪数组(有下标、length 不能pop push 不具有迭代的特性 但是有自己的属性和方法)


推荐 使用剩余参数

区别:展开运算符和剩余参数
展开运算符(...数组),将一个数组展开
剩余参数的声明是在函数的参数中   ...形参名  
使用场景
1. 求最值
const arr = [1,2,3];
console.log(Math.max(1,2,3));//3
===
console.log(Math.max(...arr));//3
2.合并数组
const arr1 = [1,2,3];
const arr2 = [4,5,6];
const arr3 = [...arr1,...arr2];[1,2,3,4,5,6]
arr2.push(...arr1)
console.log(arr2) //[4, 5, 6, 1, 2, 3]

箭头函数

箭头函数中没有绑定this对象
可以替换某些匿名函数的使用,作为函数表达式使用。所以不存在含糊提升

const fun = () => {代码};
1 只有一个参数可以省略小括号
2 代码只有一句可以省略大括号
3 代码只有一句且是return的话 可以省略大括号和return 默认return了,。
4 箭头函数可以直接返回一个对象 要加上一个括号
	const fn = uname => ({name:uname})
	fn('zs'); 

箭头函数中只能使用剩余参数…args,不能用动态参数。

箭头函数中的this问题

// this问题
console.log(this);//window
//普通函数
function fun (){
    console.log(this);//window
}
fun();//window.fun();
// 对象中的this
const obj = {
    uname:'zs',
    sayHi:function(){
        console.log(this); // obj
    },
    // 对象中的箭头函数
    sayBye:()=>{
        console.log(this); // window
    },
    work: function(){
		const fun= ()=>{
        	console.log(this); // obj 箭头函数的上一级作用域是function
        }
        fun();
	}
};
// 匿名函数中的this是谁调用指向谁
obj.sayHi();
// 箭头函数找上一级作用域中的this obj没有作用域,所以就找到了script
obj.sayBye();
// 普通箭头函数
const fn = ()=>{
    console.log(this);//window
}
fn();

对象和箭头函数中没有this对象
普通函数中有this对象,匿名函数中也有,指向的是调用者
在这里插入图片描述
DOM事件回调中不推荐使用箭头函数,因为就不能找到this

1.3 解构赋值

数组解构

是将数组元素快速批量的赋值为一系列变量。

const [变量名,变量名…] = 数组
会依次进行赋值 所有变量都属于const类型
变量<数组.length 会把有的数据赋值过去。也可以用剩余参数来接受剩下来的数组中的值 const [变量名,变量名, …args] = 数组
变量>数组.length 没有赋值的为undefined,防止传过来的是undefined,const [变量名= 0 ,变量名= 0] = 数组
按需导入const [变量名1,,变量名2,变量名3] =[1,2,3,4];
变量名1 = 1 变量名2 = 3 变量名3=4

应用:
交换值

let a = 1
let b = 2;
[b,a] = [a,b];
  1. 声明的时候一定要是let
  2. 声明好后一定要分号隔开

多维数组结构

const [a,b,[c,d]] = [1,2,[3,4]]

JS中必须要加分号的情况
多个立即执行函数要加分号
数组结构时 数组开头,特别时前面有语句的一定要加分号 ;[b,a] = [a,b]


对象解构

对象属性和方法快速批量赋值给一系列变量的简洁语法
对象解构中要求key值和变量名要一样(可以用:重命名)


const {uname:name, age,gender} = {uname:'zs',age:18}
console.log(name);
console.log(age);
console.log(age);
console.log(gender);// undefined 找不到对应的可以值就为undefined 

对象数组对象解构

const list = [
    {
        name:'ls',
        age:19
    },
    {
        name:'ww',
        age:20
    },
];
const [{name:username1,age:userage1},{name:username2,age:userage2}] = list;
console.log(username1);
console.log(userage1);
console.log(username2);
console.log(userage2);

多级对象解构

const obj=
    {
        name:'ls',
        age:19
        hobby: {
        	name:'swim',
        	howlong:3
       	}
    };
const {name,age,hobby:{name:hobbyName,howlong}}=obj;
console.log(name);
console.log(age);
console.log(hobbyName);
console.log(howlong);


2.构造函数、数据常用函数

2.1 构造函数

构造函数基本使用

构造函数是一种特殊的函数,用来初始化对象。在构造函数中,把公共的数据封装在里面

// 构造函数
function Pig(name,age,gender){
    this.name = name;
    this.age = age;
    this.gender = gender;
}
const peppa = new Pig('zs',18,'nan')
const Mom = new Pig('ls',20,'nv')

构造函数的约定:

  1. 首字母大写 ,只能用new来执行
  2. 使用new调用函数的行为称为实例化
  3. 构造函数内部无return
  4. 构造函数的返回值是创建的新对象
实例化的执行过程:new的时候
1 会先创建一个空的新对象
2 构造函数this会指向这个新对象
3 执行构造函数中的内容,给对象添加属性和方法
4 返回对象

实例成员和静态成员

实例对象就是实例化new出来的对象。
实例成员就是实例对象中的属性和方法。
构造函数中传入的参数全部一样但是实例对象还是相互独立互不影响。
后续在实例对象上添加方法和属性 都不会影响其他实例对象。
构造函数的属性和方法都称为静态成员
绑定在方法名上的属性和方法
静态成员只能构造函数来访问
静态方法中的this指向构造函数

2.2 内置构造函数

基本数据类型 :字符串、数值、布尔等类型有专门的构造函数这些构造函数称为包装类型。
JS底层对基本类型进行了包装

// const str = 'ddd';
const str = new String('ddd');

Object

静态方法
Object.keys(对象名); 获取到对象中的所有key,数组
Object.values(对象名);获取到对象中所有的value,数组
Object.assign(新对象,被克隆的对象);对象的克隆。


Array

forEach 遍历数组 没有返回值,遍历数组元素,不能中断
filter 过滤数组 返回新数组,返回筛选满足条件的数组元素
map 迭代数组 返回新数组 返回的是处理之后的数组元素
reduce 累加器 返回累计处理结果,用于求和。

arr.reduce(function(上一次值,当前值){},起始值)

// 无初始值 
arr.reduce(function (prev,current){
	return prev+current;
})

// 有初始值
arr.reduce((prev,current)=> prev+current,10)
reduce 执行过程
1. 没有初始值,则上一次值以数组的第一个数组元素的值
2. 每一次循环,把返回值作为下一次循环的上一次值
3. 如果有初始值,则初始值为上一次的值

Array中其他的实例方法

实例方法.join 数组元素拼接称字符串,返回字符串。
实例方法.find 查找元素,返回符合测试条件的第一个元素值,没有找到就返回undefined。

const mi = arr.find(item =>  item.name==='xx') 返回符合xx的元素

实例方法.every 遍历数组所有元素是否都符合指定条件,如果所有都符合就返回true,否则为false
arr.every(item=>{
	return item>40; // 元素 都大于40就返回true
}); 

实例方法.some 遍历数组所有元素,如果有一个符合就返回true,否则为false
实例方法.concat 合并两个数组,返回生成新数组
实例方法.sort 对原数组进值进行排序
实例方法.splice 删除
实例方法.reverse 反转数组
实例方法.findIndex 查找元素的索引值

静态方法
Array.from(伪数组名称); 将伪数组转换成真数组


String

属性
实例对象.length 获取字符串的长度
方法
实例对象.split(‘分隔符’) 按照分隔符把字符串拆成数组
实例对象.substring(需要截取的第一个字符的索引[,结束索引])
实例对象.startsWith(检测字符串[,检测索引号])判断是否以检测字符开头
实例对象.includes(检测字符串[,检测索引号])判断一个字符串是否包含另外一个字符串中
实例对象.toUpperCase
实例对象.toLowerCase
实例对象.indexOf
实例对象.endWith
实例对象.replace
实例对象.match


Number

实例对象.toFixed(num); // num固定几位小数。 四舍五入

3.深入面向对象

面向对象三大特性:封装(构造函数) 继承 多态

3.1 构造函数

js面向对象可以通过构造函数实现封装。
构造函数体现了面向对象的封装特性。构造函数创建的对象彼此独立,互不影响。
构造函数有一个缺点:存在浪费内存的问题。
在这里插入图片描述
因为构造函数创建的对象都是相互独立的,所以对象中的function都是不一样的。

3.2 原型对象 构造函数.prototype

每一个构造函数都有prototype属性。这个属性指向另一个对象(原型对象)
构造函数.prototype === 原型对象

原型对象上可以挂载函数,对象实例化不会多次创建原型上的函数,节约空间
构造函数.prototype.函数名=function(){}

所有实例化对象都指向同一个原型对象

不变的方法定义在prototype对象上,这样所有实例化对象都共享这些方法。

构造函数和原型对象中的this都指向实例化的对象。

let that;
function Star(name){
	that = this;
	this.name = name;
}
const lf = new Star('lf');
console.log(lf === that); // true
// 构造函数的this指向实例对象
let th;
Star.prototype.sing = function(){
	th = this
}
lf.sing();
console.log(th === lf);
// 原型对象中的this指向实例对象

给Array实例对象增加求和和求最大值的方法

Array.prototype.sum = function(){
     return this.reduce((prev,current)=> prev+current);
}
Array.prototype.max = function(){
    return Math.max(...this);
}

constructor属性

原型对象中有constructor属性。
作用:指向原型对象的构造函数
使用场景:
就是给原型对象进行赋值的时候采用的是对象 形式赋值。需要显式指明constructor指向构造函数

function Star(name){
	that = this;
	this.name = name;
}
console.log(Star === Star.prototype.constructor); // true

// 使用场景
Star.prototype = {
	constructor: Star,
	sing: function(){console.log('sing')},
	dance: function(){console.log('dance')}
}

对象原型 [[prototype]] or proto

实例对象为何能够访问原型对象,或者说 实例对象上为什么可以使用原型对象上的属性和方法,因为实例对象上有对象原型
在这里插入图片描述


原型继承

JS中大多都是借助原型对象实现继承的特性。

// 父类
function Human(){
    this.eyes= 2;
    this.mouse = 1;
}

// 子类
function Woman(){

}
// 使用原型对象实现继承 不能直接把Human赋值给原型对象,因为这样所有都是用的是同一个内存中的数据 ,这样赋值过后就没有了constructor属性
Woman.prototype = new Human();
// 构造函数指向Woman
Woman.prototype.constructor = Woman;
// 绑定woman的个别方法
Woman.prototype.baby = function(){
    console.log('5555');
}

// 子类
function Man(){

}
Man.prototype = new Human();
Man.prototype.constructor = Man;
Man.prototype.go = function(){
    console.log('gogogo');
}

console.log(new Woman());
console.log(new Man());

实例对象的对象原型__proto__指向 原型对象。这里实例化对象的对象原型为human是因为赋值的时候给了Human
原型对象中有对应的属性和方法、构造函数,

在这里插入图片描述

tips
1. 实例化的对象相互独立
2. 原型对象上直接赋值会把原本的constructor给覆盖了,所以要重新指回去。

原型链

只要是对象,就有对象原型__proto__,原型__proto__指向原型对象
实例化对象、原型对象都是对象,他们都有__proto__对象原型
Object.prototype.proto === null;

当访问一个对象的属性时,首先会查找这个对象自身有没有该属性。
如果没有就查找它的原型对象(也就是__proto__指向的原型对象)	
如果还没有找到就查找原型对象的原型对象,一直往上找,直到找到Object.prototype.__proto__=null为止
__proto__对象原型的意义就在于为对象成员查找机制提供一个方向,
instanceof运算符用于检测构造函数的prototype属性是否出现在某个实例对象的原型链上(判断是否在一条原型链上)

4.高阶技巧

4.1 深浅拷贝

拷贝只针对复杂类型的数据。
拷贝与赋值的区别:对象的赋值,他们在堆中都指向的时同一个地址中的内容,改变一个另外一个也会跟着改变。
浅拷贝

浅拷贝:拷贝的是地址。

拷贝对象: Object.assign();  //  展开运算符 {...obj}拷贝对象
拷贝数组: Arrayprototype.concat();  //  [...arr]

存在问题:对于对象中的值为简单类型的时候,会直接赋值,但是对于对象中的复制类型的数据时,就会把复杂类型的数据的地址进行赋值,所以,浅拷贝中对于复杂类型数据无法进行更深的拷贝


深拷贝

  1. 通过递归实现深拷贝

function deepClone(newObj,OldObj){
    for (let item in OldObj){
        // 一定要先判断数组之后
        if(OldObj[item] instanceof Array){
            newObj[item] = [];
            deepClone(newObj[item],OldObj[item]);
        }
        else if(OldObj[item] instanceof Object){
            newObj[item] = {};
            deepClone(newObj[item],OldObj[item]);
        }else{
            newObj[item] = OldObj[item];
        }
    }
}
  1. lodash中的cloneDeep
    const 新对象 = _.cloneDeep(被复制的对象);
  2. 通过JSON.Stringify()实现
    把对象转换成字符串
    再把这个字符串解析成对象,就是一个新的独立的对象
    const new = JSON.parse(JSON.Stringify(old));

4.2 异常处理

throw 抛异常

throw抛出异常,程序终止
throw后面跟着的是错误提示信息
 throw new Error('错误的提示信息')

try/catch/finally捕获异常

把可能会发生错误的代码写在try中
catch(e) 捕获异常
finally 不管有没有异常都会执行

4.3 处理this

this指向

普通函数this指向 : 谁调用指向谁

 function fn(){
	console.log(this)// window
}

严格模式

 'use strict'
 function fn(){
	console.log(this)// undefined
}

箭头函数this指向: 箭头函数没有this指向。this指向外层的this
**原型对象上绑定函数用箭头函数的时候,this也是指向window **
构造函数、原型对象上的函数、dom事件函数最好不用箭头函数
沿用上一层的this函数的时候用箭头函数


改变this

call() 绑定this并且调用函数,返回值是函数执行结果

 函数名.call(绑定新this对象,函数运行时所需要的参数)
const obj = {
	uname:'zs'
}
function fn(a){
	console.log(a);  //1
	console.log(this); // obj
}
fn.call(obj,1)

····
bind 绑定,返回值是绑定了新的this对象的函数

const obj = {
	uname:'zs'
}
function fn(a,b){
	console.log(a);  //1
	console.log(b);  //2
	console.log(this); // obj
}
const fun = fn.bind(obj);

document.querySelector('button').addEventListener('click',function(){
setTimeout(function(){}.bind(document.querySelector('button')),1000)
})

apply 绑定this对象, 会调用,参数要存放在数组中,返回值是函数的执行结果

const obj = {
	uname:'zs'
}
function fn(a,b){
	console.log(a);  //1
	console.log(b);  //2
	console.log(this); // obj
}
fn.apply(obj,[1,2,3])

4.4 性能优化

防抖

单位时间内,频繁触发事件,只执行最后一次。
在这里插入图片描述
使用场景:
搜索框搜索输入,只需用户最后一次输入完后,再发送请求
表单验证 最有一次输入完后再验证。

案例

const div = document.querySelector('div');
        div.addEventListener('mousemove',fn);
        let i = 0;
        let id;
        function fn(){
        // 每次触发事件就会调用这个函数,每次触发把延时函数取消,再重新注册函数
            clearTimeout(id);
            id = setTimeout(()=>{
                div.innerHTML = i++;
                clearTimeout(id);
            },500);
        }

防抖函数实现方式

lodash的防抖函数 _.debounce(function,[wait=0])

const div = document.querySelector('div');
div.addEventListener('mousemove',_.debounce(fn,500));
let i = 0;
function fn(){
    div.innerHTML = i++;
}

手写防抖函数: time时间内,调用了一段time内再次调用就清除定时器,过完了时间后执行

function debounce(fun,wait){
	let id;
	return function(){
		if(id)
			clearTimeout(id); // 在setTimeout()外部所以可以删除定时器
    	id = setTimeout(()=>{
        	fn();
        	id = null;
   		},wait);
	}
}

节流

单位时间内,频繁触发事件,只执行一次。
在这里插入图片描述


function throttle(fun,wait){
    let id=null;
    return function(){
        if(id ===null){
            id = setTimeout(function(){
            	fun();
            	id = null;// 在setTimeout()外部所以不可以删除定时器
            },wait)
        }
    }
}

防抖和节流总结

在这里插入图片描述
防抖:类似于王者中的回城操作,中间有操作就会重新计时
节流:类似于王者中的技能,技能发动一次,如何在冷却时间时候点击无效


Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐