变量根据作用域的不同分为两种:全局变量和局部变量
一、什么是闭包
闭包是指有权访问另一个函数作用域中变量的函数。简单理解就是,一个作用域可以访问另外一个函数内部的局部变量。
// sayNum函数作用域访问了另外一个函数say里面的局部变量num,此时产生了闭包,say就是一个闭包函数
function say(){
var num = 2;
function sayNum(){
console.log(num);
}
sayName();
}
say();
闭包的主要作用:延伸了变量的作用范围
闭包应用
eg:点击li打印当前索引
var lis = document.querySelectorAll("li")
for(var i=0; i< lis.length;i++){
(function(i){
lis[i].onclick = function(){
console.log(i)
}
})(i)
}
eg:循环中的setTimeout,3s之后打印所有li元素的内容
var lis = document.querySelector("ul").querySelectorAll("li")
for(var i=0; i < lis.length;i++){
(function(i){
setTimeout(function(){
console.log(lis[i].innerHTML)
},3000)
})(i)
}
eg:闭包应用-计算打车价格
打车起步价13(3公里内),之后每多一公里增加5块钱,用户输入公里数就可以计算打车价格,如果有拥堵情况,总价格多收取10块钱的拥堵费
var car = (function(){
var start = 13;
var total = 0;
return {
price:function(n){
if(n<=13){
total = 13
} else {
total = 13 + (n-3)*5
}
return total
},
price1:function(flag){
return flag ? total+10 : total
}
}
})()
car.price(5)
car.price1(true)
二、什么是递归?
如果一个函数在内部可以调用其本身,那么这个函数就是递归函数,简单理解函数内部自己调用自己,这个函数就是递归函数。由于递归很容易发生”栈溢出”错误,所以必须要加退出条件return。
递归作用:和循环效果一样,可以重复多次执行同一段代码,只不过在此时是重复不断的调用函数而已,使用这个递归函数最大的问题就是注意一定加一个退出条件,否则会不断的创建开辟我们的作用域不断的创建我们的内存,就会造成栈溢出。
function say(){
say()
}
say()
// Uncaught RangeError: Maximum call stack size exceeded
改造
// 递归函数:函数内部自己调用自己,这个函数就是递归函数
var num = 1;
function sayHello(){
console.log("hello world")
if(num == 6) {
return; // 递归里面必须加退出条件
}
num++;
sayHello();
}
sayHello();
例1:利用递归求1~n的阶乘,求1*2*3*…*n阶乘
function num(n){
if(n == 1) {
return 1;
}
return n * num(n-1)
}
num(3);// 输出6
// 详细思路:假如用户输入的是3
// return 3*num(3-1)
// return 3*2*num(2-1)
// return 3*2*1
// 6
例2:求斐波那契数列(兔子序列)前两项相加等于第三项的和:1、1、2、3、5、8、13、21…
用户输入一个数字n就可以求出这个数字对应的兔子序列值
// 只需要知道用户输入的n的前面两项(n-1 n-2)就可以计算出n对应的序列值
function num(n){
return n === 1 || n === 2 ? 1 : num(n-1) + num(n-2)
}
num(3); // 输出2
num(6); // 输出8
例3:根据id返回对应的数据对象
var data = [
{
id:1,
name:"家电",
goods:[
{
id:11,
gname:"冰箱",
goods:[
{
id:111,
gname:"海尔"
},
{
id:112,
gname:"美的"
}
]
},
{
id:12,
gname:"洗衣机"
}
]
},
{
id:2,
name:"服饰"
}
]
// 利用forEach去遍历里面的每一个对象
function getObjById(json,id){
var obj = {}
json.forEach(function(item){
if(item.id == id) {
obj = item
} else if(item.goods && item.goods.length > 0) {
obj = getObjById(item.goods,id)
}
})
return obj;
}
getObjById(data,1)
getObjById(data,11)
三、浅拷贝和深拷贝
浅拷贝和深拷贝是只针对Object和Array这样的引用数据类型的。
浅拷贝只复制指向某个对象的指针,而不复制对象本身,新旧对象还是共享同一块内存。但深拷贝会另外创造一个一模一样的对象,新对象跟原对象不共享内存,修改新对象不会改动原对象
浅拷贝:
1、如果是引用数据类型,名字存在栈内存中,值存在堆内存中,但是栈内存会提供一个引用的地址指向堆内存中的值,如果拷贝的对象更改了值,被拷贝的对象也会同样修改,因为两者指向的是同一个内存空间看下图说明浅拷贝:
当b=a进行拷贝时,其实复制的是a的引用地址,而并非堆里面的值。
而当我们a[0]=1时进行数组修改时,由于a与b指向的是同一个地址,所以自然b也受了影响,这就是所谓的浅拷贝了。
2、实现浅拷贝的方法:
a、for…in只循环第一层
var obj = {
id:1,
name:"andy",
msg:{
age:18
}
}
var o = {}
for(var k in obj) {
o[k] = obj[k]
}
o.msg.age = 20
console.log(o)
console.log(obj)
// ES6语法糖方式使用浅拷贝
Object.assign(o,obj);
console.log(o)
b、直接用=赋值
// 对象赋值
var obj1 = {
name: 'zhangsan',
age: '18',
language: [1,[2,3],[4,5]],
};
var obj2 = obj1;
obj2.name = "lisi";
obj2.language[1] = ["二","三"];
console.log('obj1',obj1)
console.log('obj2',obj2)
// obj1和obj2中的name以及language属性对应的值改变
运行结果:
c、Object.assign()
可以把任意多个的源对象自身的可枚举属性拷贝给目标对象,然后返回目标对象。但Object.assign()进行的是浅拷贝,拷贝的是对象的属性的引用,而不是对象本身。
var objA = { a: {a: "kobe", b: 39} };
var objB= Object.assign({}, objA);
objB.a.a = "wade";
console.log(objA.a.a); // wade
注意:当object只有一层的时候,是深拷贝
let obj = {
username: 'kobe'
};
let obj2 = Object.assign({},obj);
obj2.username = 'wade';
console.log(obj);//{username: "kobe"}
JS 中有个重要的类型叫做引用类型。这种类型在使用的过程中,因为传递的值是引用,所以很容易发生一些副作用,比如:
let a = { age: 1 }
let b = a
b.age = 2
console.log(a);// {age:2}
console.log(b);// {age:2}
上述代码的写法会造成a和b的属性都被修改了。在日常开发中肯定不想出现这种情况,所以都会用上一些手段去断开它们的引用连接。对于上述的数据结构来说,浅拷贝就能解决我们的问题。
let x = { age: 1 }
let y = { ...x }
y.age = 2
console.log(x);// {age:1}
console.log(y); // {age:2}
但是浅拷贝只能断开一层的引用,如果数据结构是多层对象的话,浅拷贝就不能解决问题了,这时候我们需要用到深拷贝。
深拷贝:拷贝多层,每一级别的数据都会拷贝。
实现思路:obj中有个msg,msg会新开辟一个内存空间深拷贝浅拷贝,深拷贝会把新开辟的空间重新复制一份新的空间,然后把拷贝完的空间赋值给o,这样两者互不干扰。这样修改了o的数据就不会影响obj的数据
实现方式1:利用函数递归
递归方法实现深度克隆原理:遍历对象,数组直到里边都是基本数据类型,然后再去复制,就是深度拷贝,需要注意的是,如果对象比较大,层级也比较多,深拷贝会带来性能上的问题。在遇到需要采用深拷贝的场景时,可以考虑有没有其他替代的方案。在实际的应用场景中,也是浅拷贝更为常用
采用递归去拷贝所有层级属性封装方法1:
var obj = {
id:1,
name:"andy",
msg:{
age:18
},
["pink","red"] :
}
var o = {}
封装函数
function deepCopy(newObj,oldObj){
k in oldObj){
// 判断属性值属于哪种数据类型
获取属性值
item = oldObj[k]
// 2、判断这个值是否是数组
instanceof Array){
newObj[k] = []
deepCopy(newObj[k],item)
else if(item instanceof Object){
// 3、判断这个值是不是对象
newObj[k] = {}
deepCopy(newObj[k],item)
else {
// 4、属于简单数据类型
item; =
}
}
}
deepCopy(o,obj)
20 =
console.log(obj)
采用递归去拷贝所有层级属性封装方法2:
function deepClone(obj){
let objClone = Array.isArray(obj)?[]:{};
if(obj && typeof obj==="object"){
for(key in obj){
if(obj.hasOwnProperty(key)){
//判断ojb子元素是否为对象,如果是,递归复制
if(obj[key]&&typeof obj[key] ==="object"){
objClone[key] = deepClone(obj[key]);
}else{
//如果不是,简单复制
objClone[key] = obj[key];
}
}
}
}
return objClone;
}
let a=[1,2,3,4],b=deepClone(a);
a[0]=2;
console.log(a);// [2,2,3,4]
console.log(b);// [1,2,3,4]
实现方式2:JSON.parse(JSON.stringify(obj))
原理:用JSON.stringify将对象转成JSON字符串深拷贝浅拷贝,再用JSON.parse()把字符串解析成对象,一去一来新的对象就产生了,而且对象会开辟新的栈,实现深拷贝
let arr = [1, 3, {
username: ' kobe'
}];
let arr4 = JSON.parse(JSON.stringify(arr));
arr4[2].username = 'duncan';
console.log(arr, arr4)
缺点:只能拷贝对象和数组,无法拷贝函数,无法拷贝原型链上的属性和方法(Date, RegExp, Error等)
let arr = [1, 3, {
username: ' kobe'
},function(){}];
let arr4 = JSON.parse(JSON.stringify(arr));
arr4[2].username = 'duncan';
console.log(arr, arr4);
这是因为JSON.stringify()方法是将一个JavaScript值(对象或数组)转换为一个JSON字符串,不能接受函数。
深拷贝在业务里的最常见的应用场景:
举个栗子,业务需求是 : 一个表格展示商品各种信息,点击【同意】时,是可以弹出对话框调整商品数量的。这种业务需求下,我们就会用到对象的深拷贝。
如果使用浅拷贝存在的问题如下图:当我们修改【调整商品表格】里的商品数量时,【商品表格】也跟着改变了。
// 表格对象的数据结构
var tableArr = [
{goods_name : '长袖' , code : 'M2160864' , num : '2'},
{goods_name : '背夹' , code : 'M2160B0170' , num : '3'},
{goods_name : '短裤' , code : 'M21604106' , num : '3'},
]
var adjustTableArr = [] // 调整表格用的数组
for (var key in tableArr) { // 浅拷贝
adjustTableArr[key] = tableArr[key]
}
而实际上,我们希望这2个表格里的数据完全独立,互不干扰,只有在确认调整之后才刷新商品数量。这种情况下我们就可以使用前面说的深拷贝
var adjustTableArr = JSON.parse(JSON.stringify(tableArr))