闭包是什么
闭包就是函数的嵌套,内层函数访问了外层函数的局部变量
闭包有什么用
实现变量私有化,让外面的人无法访问内部变量
闭包会产生什么现象和问题
- 内存泄漏
- 本该被释放的变量,无法及时释放,会存在闭包空间中
变量提升(仅存在于var声明变量)
- 变量提升出现在当前作用域的最前面
- 提升时,只提升变量声明,不提升变量赋值
- let/const声明的变量不存在变量提升
- 实际开发中推荐先声明在访问变量
函数提升(函数提升与变量提升比较相似)
- 函数提升只提升声明,不提升调用
- 函数表达式不存在提升的现象
函数声明
function fn(){}
函数表达式,匿名函数
var fn = function(){}
arguments对象的使用
'use strict'
//需求:求和函数,参数不限数量
//解决方案:arguments伪数组
function sum(){
//开启严格模式,无法使用arguments
/*
arguments非常特殊,只存在于function函数中
这个伪数组中装着所有的实参
伪数组用不了数组的方法 forEach
*/
console.log(arguments);
let sum = 0;
for (let index = 0; index < arguments.length; index++) {
sum +=arguments[index]
}
return sum
}
console.log(sum(1,2));
console.log(sum(1,2,3));
/*
为什么现在不用arguments了?
1.是可变参数的出现ES6新特性
2.性能原因
*/
function fn1(a){
arguments[0] = 50
console.log(arguments[0])//50
//arguments是动态伪数组,如果内部的数据发生改变,会自动同步到形参
console.log(a);//50
}
fn1(10)
/*
聊一聊arguments这个对象
arguments就是一个伪数组,仅存在于function中,它的
作用是装载着所有的实参,因为arguments是动态伪数组,
数据变化带来的性能损耗较大,每次都会同步更新所有参数,
如果参数过多则会出现问题,在开启严格模式后就会彻底禁用arguments,
现在开发中基本不会arguments了,可以使用可变参数完全替代arguments的功能
*/
剩余参数(…) ES6语法新特性 2015年提出
- …是语法符号,置于最末函数形参之前,用于获取多余的实参
- 借助…获取的剩余实参,是个真数组
- 剩余参数支持箭头函数,开发过程中提倡多使用剩余参数
剩余参数:函数参数使用,把多个元素收集起来生成一个真数组(凝聚)
展开运算符:将数组展开成各个元素(拆散)
箭头函数(重要)
- 当箭头函数只有一个参数时,可以省略参数的小括号,其余个数不能省略(没有参数也需要加上小括号)
- 当箭头函数的函数体只有一句代码 可以省略函数体的大括号,这句代码就是返回值(可以不用写return)
- 如果返回的是个对象,则需要把对象用小括号包裹
- 箭头函数里面没有arguments,但是有剩余参数
箭头函数中的this指向
function函数中的this指向调用者
箭头函数的this指向和当前作用域的this指向一样,它没有自己的this指向
箭头函数的this指向取决于当前箭头函数定义的位置,不看是谁调用的箭头函数
一般事件处理源函数不要用箭头函数,因为普通的function函数,更容易让我们找到事件源
优先使用箭头函数,若不行则使用function函数
const fn = () => console.log(1);
fn()
let fn1 = (a,b) => (a+b)
console.log(fn1(1,2));
function fn2(){
console.log(this);//window
}
fn2()
setTimeout(()=>console.log(this),1000)//window
document.querySelector("button").addEventListener('click',function(){
console.log(this);//button
setTimeout(()=>{
console.log(this);//button
},1000)
})
构造函数
构造函数是一种特殊的函数,专门用于帮助程序员创建对象的
构造函数本质就是普通函数,只有在使用new关键字调用它的时候,就被称为构造函数
定义时和普通函数没有任何区别,只有在调用时才能决定是否是构造函数
特点:
- 调用构造函数会自动创建一个新对象
- 构造函数中的this指向这个新对象
- 执行函数的代码,可以给当前创建的新对象添加属性
- 默认返回这个新对象,不需要写return
构造函数new实例化执行过程
- 创建空对象
- 构造函数this指向新对象
- 执行构造函数代码
- 返回新对象
实例成员(属性和方法)写在谁身上?
实例对象的属性和方法即为实例成员(new 构造函数就是实例对象)
实例对象相互独立,实例成员当前实例对象使用
静态成员(属性和方法)写在谁身上?
构造函数的属性和方法被称为静态成员
静态成员只能构造函数访问
function Person(name,age){
//实例成员(属性和方法)
this.name = name;
this.age = age;
this.dance = function(){
console.log("跳舞");
}
}
const zs = new Person("张三",18)//实例对象
zs.dance()
console.log(zs);//实例成员只能当前实例对象使用
//静态成员(属性和方法)
Person.hobby = '看书'
Person.swim = function(){console.log('游泳');}
console.log(Person.hobby);
Person.swim();//静态成员只能构造函数访问
Object静态方法
let person = {
name:'张三',
age:19
}
for(let k in person) {
console.log(k);//键
console.log(person[k]);//值
}
//获取对象中所有key Object.keys() 返回数组
console.log(Object.keys(person));
Object.keys(person).forEach(k=>console.log(k));
//获取对象中所有的值 Obejct.values() 返回数组
console.log(Object.values(person));
Object.values(person).forEach(k=>console.log(k));
//Object.assign()
//将源对象的所有属性复制给目标对象 返回新对象
//参数一:目标对象 参数二:源对象
const o = Object.assign({},person)
console.log(o);
常用数组方法
静态方法Array.from()将伪数组转换成真数组
常用字符串方法
number.toFixed(x) toFixed() 方法可把 Number 四舍五入为指定小数位数的数字。
什么是面向过程?
按照我们分析好了的步骤,按照步骤一步一步的解决问题
什么是面向对象
以对象功能来划分为一个个对象,然后由对象之间分工和合作
两种编程思想的特点
面向过程性能高,但是不灵活,复用性低
面向对象灵活,易扩展,易维护,但是性能比面向过程低
构造函数方法很好用,但是存在浪费内存的问题
function Person(name,age){
this.name = name;
this.age = age;
this.hi = function(){console.log('hi');}
}
const zs = new Person('zs',18)
const ls = new Person('ls',18)
/*
原型对象:
所有的构造函数都会有一个属性:prototype
他是一个对象,我们称它为原型对象
特点:
1.所有通过构造函数创建出来的实例对象,都可以访问原型对象中的成员
2.实例对象创建时,不会重复创造原型对象,每个构造函数的原型对象是唯一的
3.对象会优先找自己身上的属性和方法,找不到才去原型上查找属性和方法,就近原则
*/
console.log(Person.prototype);
Person.prototype.name = '韩伟'
//原型对象中方法的this指向:调用者,实例对象
//1.构造函数不能是箭头函数,因为箭头函数的this指向和当前环境有关
//2.原型的函数要根据情况判断,如果用了箭头函数意味着this不再指向实例对象
Person.prototype.sayHi = function(){console.log('你好');console.log(this);}
zs.sayHi()
ls.sayHi()
console.log(zs.hi === ls.hi);
console.log(zs.sayHi ===ls.sayHi);
const arr = ['12']
console.log(arr);
console.log(Array.prototype);
/*
原型对象是什么?
prototype,每个构造函数都有原型对象
原型对象的作用是什么?
可以把那些公共的属性和方法,直接定义在prototype对象上
实例对象可以直接访问原型对象中的属性和方法
这些属性和方法不会多次内存中创建,从而节约空间
*/
constructor属性的作用
function Person(name,age){
this.name = name;
this.age = age;
}
Person.prototype.hobby = function(){console.log(this);}
//constructor属性在原型对象里面
//constructor属性的作用是指向该原型对象的构造函数
console.log(Person.prototype);
console.log(Person.prototype.constructor);
//批量往原型对象上加方法和属性
Person.prototype = {
constructor:Person,
sing(){console.log('唱歌');},
sing1(){console.log('唱歌');},
sing2(){console.log('唱歌');}
}
console.log(Person.prototype);
对象中的原型__proto__
function Person(name, age) {
this.name = name;
this.age = age;
this.hi = function () { console.log('hi'); }
}
const zs = new Person('zs', 18)
const ls = new Person('ls', 18)
console.log(Person);
console.log(zs);
console.log(Person.prototype);
//__proto__不是标准属性,在浏览器已经将其弱化显示[[Prototype]]:
console.log(zs.__proto__);
console.log(Person.prototype == zs.__proto__);
console.log(Person.prototype === zs.__proto__);
/*
prototype是什么?哪里来?
原型对象
构造函数都自动有原型对象
__proto__属性是什么?在哪里?指向谁?
原型
在实例对象里面
指向prototype原型对象,这样实例对象就可以访问原型对象里面的方法
*/
原型链
function Person(name, age) {
this.name = name;
this.age = age;
}
const zs = new Person('zs', 18)
Object.prototype.sayHi = function(){console.log("Object上的prototype");}
zs.sayHi()
console.log(zs.__proto__ === Person.prototype);true
console.log(Person.prototype.__proto__ === Object.prototype);//true
console.log(Object.prototype.__proto__);//null
/*
原型链(对象属性的查找规则)
实例对象中找属性和方法时,如果找不到,会找__proto__
原型对象中也找不到,再寻找原型对象的__proto__
逐级向上查找,直到Object的原型对象为止(null),如果都找不到则没有
*/
instanceof运算符
function Person(name, age) {
this.name = name;
this.age = age;
}
const zs = new Person('zs', 18)
/*
instanceof运算符返回的是布尔值
instanceof一般可用于判断具体的引用数据类型,比typeof更准确,typeof一般只用于判断基本数据类型
实例对象 instanceof 构造函数 ,判断构造函数的原型对象是否在实例对象的原型链上
也可以通俗理解 当前的对象,是否是构造函数原型底下的孩子
*/
const arr = '123'
const arr1 = []
console.log(typeof arr);
console.log(typeof arr1);//typeof判断不了引用数据类型
console.log(arr1 instanceof Array);
原型继承
function Man(){}
function Woman(){}
function Person(){
this.age = 2
}
Person.prototype.eat=function(){console.log('吃饭');}
Man.prototype = new Person()
Man.prototype.constructor = Man
Woman.prototype = new Person()
Woman.prototype.baby=function(){console.log('wa')}
const m1 = new Man()
const w1 = new Woman()
console.log(Man.prototype);
m1.eat()
w1.eat()
w1.baby()
/*
原型继承总结:
1.创建父级构造函数
2.将所有公共的方法放到父级的原型对象上
3.将子级构造函数的原型对象 指向 父级构造函数创建的实例对象
注意:因为对象覆盖了原型对象,所有再把constructor指回当前构造函数
Man.prototype = new Person()
Man.prototype.constructor = Man
*/
完整图解
浅拷贝
const obj = {
name:'zs'
}
//对象浅拷贝
//1.Object.assign()
//const newObj = Object.assign({},obj)
//2.展开运算符
const newObj = {...obj}
obj.name = 'ls'
console.log(obj);
console.log(newObj);
const arr = ['1','2']
//数组浅拷贝
//1.concat方法合并一个空数组
// const newArr = [].concat(arr)
//2.展开运算符
const newArr = [...arr]
arr[1] = '3'
console.log(arr);
console.log(newArr);
/*
总结:
对象和数组的浅拷贝就是把对象里的属性和值复制一份,放到新对象中存储,
新老对象是不同的地址值,所以修改其中一个对象,不会影响另一个对象的属性
问题:
浅拷贝就是只拷贝对象第一层的数据,如果全部都是基本数据类型则没有任何问题
只要有引用数据类型的数据,将会将其地址值复制一份,导致两个对象的属性共用同一个对象
*/
深拷贝的两种方法
const obj = {
name:'zs',
family:{
name:'ls'
},
love:undefined,
play(){
console.log('玩耍');
}
}
//需求:拷贝一个新对象,修改对象内的引用数据类型属性,也不会对原对象有任何影响
//1.利用JSON的序列化和反序列化实现
//序列化:JSON.stringify(obj)
//反序列化:JSON.parse(obj)
/*注意事项:
使用JSON.stringify和JSON.parse进行深拷贝 无法拷贝undefined和function
*/
const newObj = JSON.parse(JSON.stringify(obj))
console.log(obj === newObj);
obj.family.name = '王五'
console.log(obj);
console.log(newObj);
//2.js库lodash里面的_.cloneDeep内部实现了深拷贝
/*
参数:需要深拷贝的对象
返回值:拷贝好的新对象
优点:连同undefined和function函数一并拷贝过来
缺点:需要额外引入一个第三方库,增加项目体积
*/
递归实现深拷贝
/*
深拷贝思路:
1.深拷贝的核心就是利用函数递归
2.封装函数,里面先判断拷贝的是数组还是对象
3.然后开始遍历
4.如果属性值是引用数据类型(比如数组或者对象),则再次递归函数
5.如果属性值是基本数据类型,则直接赋值
*/
const obj = {
name:'zs',
family:{
name:'ls'
},
love:undefined,
play(){
console.log('玩耍');
}
}
function cloneDeep(oldObj){
//1.先判断要拷贝的是对象还是数组
let newObj = Array.isArray(oldObj) ? [] : {}
//2.遍历老对象,把属性复制到新对象中
for (const k in oldObj) {
newObj[k] = typeof oldObj[k] === 'object' ? cloneDeep(oldObj[k]) : oldObj[k]
}
return newObj
}
const newObj = cloneDeep(obj)
obj.family.name = 'ww'
console.log(obj);
console.log(newObj);
可以改变this指向的三种方法
function fn(a,b){
console.log(this);
console.log(a+b);
}
fn(1,2)//window 3
const obj = {
name:'zs'
}
fn.call(obj,2,3)//obj 5
//call() 方法是所有函数都能使用的方法
//1.调用函数
//2.修改函数内部的this指向
//参数1:this要指向的对象
//参数2~参数n(剩余参数)
fn.apply(obj,[1,2]) //obj 3
//apply()参数1:this要指向的对象
//参数2:必须是数组,可以把原方法需要的参数全部放到数组中一起传入
//apply与call的区别仅仅只是传参方式不同
const newFn = fn.bind(obj,1,2)
newFn()//obj 3
//参数1:this要指向的对象
//参数2~参数n(剩余参数)
//返回值:新函数,该函数内部的this指向参数1
this的指向
节流防抖
防抖:单位时间内,频繁触发事件,只执行最后一次
节流:单位时间内,频繁触发事件,只执行一次
手写防抖函数:只要有定时器就要清除定时器后开启
手写节流函数:判断内存中有没有正在开启的定时器,如果有就不能再开了,如果没有才开启定时器,定时器执行时记得清除定时器id标识