内存空间
基本数据类型: 存放在栈中。引用数据类型,数据存储在堆中,变量存放在栈当中,变量中存储的是堆地址。
基本数据类型赋值时,拷贝的是数据;引用类型赋值时,拷贝的是地址。
扩展: 栈使用的是一级缓存, 他们通常都是被调用时处于存储空间中,调用完毕立即释放。Js一般用于存储基本数据类型 堆则是存放在二级缓存中,生命周期由虚拟机的垃圾回收算法来决定(并不是一旦成为孤儿对象就能被回收)。所以调用这些对象的速度要相对来得低一些。一般用于存储引用类型。
递归
递归就是在函数体内调用本函数,使用要注意函数终止条件避免死循环。
递归的调用,最终还是要转换为自己这个函数。
步骤:
1. 假设递归函数已经写好 2. 寻找递推关系 3. 将递推关系的结构转换为递归体 4. 将临界条件加入到递归体中
例如:
求1-100的和
分析: 1. 假设递归函数已经写好为sum,既sum(100),就是求1-100的和 2. 寻找递推关系: 就是 n 与 n-1 ,或 n-2 之间的关系 => sum(n) == sum(n-1) + n => sum(100)=sum(99)+100; 3. 将递归结构转换成递归体 function sum(n){ return sum(n-1) + n; } 4. 将临界条件加入到递归体中 - 求100转换为求99 - 求99转换为求98 - 求98转换为求97 ... - 求2转换为求2 - 求1转换为求1 即sum(1) = 1 所以最终函数为: function sum(n){ if(n==1) { return 1; }; return sum(n-1) + n; }
练习:
-
利用递归完成从10加到30
-
利用递归完成指定数的阶乘(6的阶乘:
6*5*4*3*2*1
) -
求 1,3,5,7,9,...第n项的结果
斐波那契数列:
斐波那契数列: 1 1 2 3 5 8 13 .....
// f(n) =f(n-1) + f(n-2)
1. 假设已知f(n)为第n项 2. 递归关系:f(n) =f(n-1) + f(n-2) 3. 将递归结构转换成递归体 function f(n){ return f(n-1)+f(n-2); } 4. 临界条件: f(0) = 1 f(1) = 1 所以最终函数为: function f(n){ if(n == 0 || n ==1) { return 1; }; return f(n-1)+f(n-2); }
深浅拷贝
浅拷贝是指复制指向某个对象的指针(地址)而不复制对象本身,新旧对象还是共享同一块内存,修改一个另一个也会改变。
深拷贝会另外创造一个一模一样的对象,新对象跟原对象不共享内存,修改新对象不会改到原对象。
常见的浅拷贝的实现方式
-
Object.assign()
-
concat()
-
扩展运算符...
-
赋值=
-
slice(了解)
深拷贝的实现方式
-
JSON.parse(JSON.stringify())序列化数组或对象
原理: 用JSON.stringify将对象转成JSON字符串,再用JSON.parse()把字符串解析成对象,新的对象产生了,而且对象会开辟新的栈,实现深拷贝。
用法:
let arr=[1,2,3,{ x: 1 }]; let str = JSON.stringify(arr); let newArr = JSON.parse(str); arr[3].x=2; console.log(arr); console.log(newArr);
缺陷:
-
序列化时会把函数和undefined丢失
-
会把NaN序列化为null
好处:
-
判断数组是否包含某对象,或者判断对象是否相等。
let arr = [ {name:'pyy'}, {name:'wyz'}, {name:'gtl'}, ]; let obj = {name:'wyz'} console.log(JSON.stringify(arr).includes(JSON.stringify(obj)));
-
判断两数组/对象是否相等
let a = [1,2,3],b = [1,2,3]; console.log(JSON.stringify(a) === JSON.stringify(b));
-
让本地存储可以存储对象。
-
递归实现深拷贝
// 深拷贝对象 function deepClone(o) { let obj = {}; for (let key in o) { // 遍历目标 if (typeof o[key] === 'object') { // 如果值是对象,就递归一下 obj[key] = {}; obj[key] = deepClone(o[key]); } else { // 如果不是,就直接赋值 obj[key] = o[key]; } } return obj; }
// 深拷贝对象、数组 function deepClone(o) { let obj = o.constructor === Array ? [] : {}; // 判断复制的目标是数组还是对象 for (let key in o) { // 遍历目标 if (typeof o[key] === 'object') { // 如果值是对象,就递归一下 obj[key] = o[key].constructor === Array ? [] :{}; // 判断复制的目标是数组还是对象 obj[key] = deepClone(o[key]); } else { // 如果不是,就直接赋值 obj[key] = o[key]; } } return obj; }
-
对象的方法
Object.assign()
Object.assign方法用于将所有可枚举属性的值从一个或多个源对象分配到目标对象。它将返回目标对象
语法:
let obj = Object.assign(target,...sources); // target: 目标对象 // sources: 源对象。 // obj: 目标对象。 // 注意:如果目标对象中的属性具有相同的键,则属性将被源对象中的属性覆盖。后面的源对象的属性将类似地覆盖前面的源对象的属性。
例子:
// 复制一个对象 const obj = { a: 1 }; const copy = Object.assign({}, obj); console.log(copy); // { a: 1 }
const o1 = { a: 1 }; const o2 = { b: 2 }; const o3 = { c: 3 }; const obj = Object.assign(o1, o2, o3); console.log(obj); // { a: 1, b: 2, c: 3 } console.log(o1); // { a: 1, b: 2, c: 3 }, 注意目标对象自身也会改变。
const o1 = { a: 1 }; const o2 = { b: 2 }; const o3 = { c: 3 }; const obj = Object.assign({},o1, o2, o3); console.log(obj); // { a: 1, b: 2, c: 3 } console.log(o1); // { a: 1 }, 注意对比,o1自身不会改变。
const o1 = { a: 1, b: 1, c: 1 }; const o2 = { b: 2, c: 2 }; const o3 = { c: 3 }; const obj = Object.assign({}, o1, o2, o3); console.log(obj); // { a: 1, b: 2, c: 3 } 后面的源对象的属性将覆盖前面的源对象的属性。
Object.defineProperty()
Object.is()
Object.is()
方法判断两个值是否为同一个值。
语法:
let result = Object.is(value1, value2);
Object.is('foo', 'foo'); // true Object.is('foo', 'bar'); // false Object.is([], []); // false let foo = { a: 1 }; let bar = { a: 1 }; Object.is(foo, foo); // true Object.is(foo, bar); // false // 特例 Object.is(0, -0); // false 0 === -0 // true Object.is(0, +0); // true Object.is(-0, -0); // true
异常
try{
校验代码是否有错(是否有异常)
}catch(error){
如果有异常,该异常的相关信息会保存在参数error中,error是一个异常对象。
console.log(error);
console.log(error.name);
console.log(error.message);
}finally{
无论有无异常,都会执行finally中的代码
}
例如:
var a =1; try{ a=2; console.log(a); console.log(b); console.log('123'); }catch(error){ console.log(error); console.log(error.name); console.log(error.message); }finally{ console.log('无论有无异常,都会执行finally中的代码'); } console.log('处理异常后程序会继续执行,而不会报错停止');
内置对象
Math
方法:
1. abs:获取绝对值 2. round:四舍五入 3. floor:向下取整 4. ceil:向上取整 5. random:0-1之间的随机数,不包含1 6. sqrt:某个数的平方根是这个值 7. pow:x的y次方 8. min:求几个数中的最小数 9. max:求几个数中的最大数
console.log(Math.abs(-9));//9 console.log(Math.round(-4.2));//-4 console.log(Math.floor(4.3));//4 console.log(Math.ceil(4.3));//5 console.log(Math.sqrt(4));//2 console.log(Math.pow(4,3));//64 console.log(Math.max(4,3,2,9));//9 console.log(Math.min(4,3,2,9));//2
Date
JavaScript唯一可以处理时间 的内置类。
方法
1. 获取当前时间 let now = new Date(); 2。 获取年月日 时分秒 时间对象名.getFullYear() ;获取年份 时间对象名.getMonth();获取月份,从0开始 0~11 表示1到12月 时间对象名.getDate();获取当月的天数 时间对象名.getHours();获取小时数,24小时制 时间对象名.getMinutes();获取分钟数 时间对象名.getSeconds();获取当前的秒数 时间对象名.getDay();获取星期几 0~6 0表示星期天 6表示星期六 时间对象名.getTime();获取当前时间和1970年1月1日0时0份0秒之间的毫秒数
toFixed
toFixed()
方法用来格式化一个数值(四舍五入得指定位数小数,去掉剩余小数)。
语法:
number.toFixed(参数)
参数:小数点后数字的个数,0 到 20 之间,省略默认为0.
let num = 12345.6789; num.toFixed(); // 12346:进行四舍六入五 num.toFixed(1); // 12345.7:进行四舍六入五 num.toFixed(6); // 12345.678900:用0填充 // 了解: 2.55.toFixed(1) // ?
2.55-2.5 // 0.04999999999999982 // 由于2.55不是精确表示的,而2.5是可以精确表示的,所以2.55 - 2.5就可以得到0.05存储的值。可以看到确实是比0.05小。 // 根本原因在于2.55的存储要比实际存储小一点,导致0.05的第1位尾数不是1,所以就被舍掉了。 // js计算精度存在问题 0.1 + 0.2 === 0.3 => false
匿名函数
匿名函数顾名思义指的是没有名字的函数。
函数声明式声明一个普通函数语法是:
function fn(){}
把名字去掉,就变成了匿名函数了。
function (){}
但是由于不符合语法要求,会报错。解决方法只需要给匿名函数包裹一个括号即可。
(function (){})
如何调用呢,见下面的立即执行函数。
匿名函数的应用场景:
-
函数表达式
//将匿名函数赋值给变量fn。 let fn= function(){ console.log('fn') }
-
返回值
function fn() { return function() { return '嘿嘿嘿' } } fn()// 拿到的是fn返回的匿名函数 fn()() // 调用fn返回的匿名函数
作用: 不必为函数命名;减少全局变量;避免变量污染;内部形成了一个单独的作用域,可以封装一些外部无法读取的私有变量。
立即执行函数(IIFE)
立即执行函数是指声明匿名一个函数,并马上调用这个匿名函数就叫做立即执行函数;也可以说立即执行函数是一种语法,让你的函数在定义以后立即执行。
JS引擎规定,如果function出现在行首,一律解析成语句。因此JS引擎看到行首是function关键字以后,认为这一段都是函数定义,不应该以原括号结尾。
解决方法就是不要让function出现在行首,让JS引擎将其理解为一个表达式,最简单的处理就是将其放在一个圆括号里。
作用:
-
不必为函数命名,避免了污染全局变量
-
立即执行函数内部形成了一个单独的作用域,可以封装一些外部无法读取的私有变量
使用的场景
-
所有的这些工作只需要执行一次,比如只需要显示一个时间。
-
你的代码在页面加载完成之后,不得不执行一些设置工作。
语法:
// 1. (function(){})() // 用括号把函数包起来 // 2 (function(){}()) //用括号把整个表达式包起来
传参:
let result = (function(c){ let a = 100,b=200; return a * b / c; }(20)); console.log(result);
返回值:
像其他函数一样,立即执行函数也可以有返回值。
let result = (function(){ let a = 100,b=200; return a * b; }()); console.log(result);
执行上下文
执行上下文就是当前 JavaScript 代码被解析和执行时所在环境的抽象概念, JavaScript 中运行任何的代码都是在执行上下文中运行。执行上下文有三种,分别是全局执行上下文,函数执行上下文,与eval函数执行上下文(一般不用)。
全局执行上下文
全局执行上下文只有一个,在客户端中一般由浏览器创建,也就是window对象。全局对象window上预定义了大量的方法和属性,我们在全局环境的任意处都能直接访问这些属性方法,同时window对象还是var声明的全局变量的载体。我们通过var创建的全局对象,都可以通过window直接访问。
函数上下文
函数执行上下文可存在无数个,每当一个函数被调用时都会创建一个函数上下文。需要注意的是,同一个函数被多次调用,每次调用都会创建一个新的上下文。
上下文种类不同,而且创建的数量还这么多,它们之间的关系是怎么样的,又是谁来管理这些上下文呢?那就来说说执行上下文栈。
执行上下文栈
执行上下文栈执行栈,在其他编程语言中也被叫做调用栈,具有 LIFO(Last In First Out后进先出,也就是先进后出)结构,用于存储在代码执行期间创建的所有执行上下文。
当 JavaScript 引擎首次读取你的脚本时,它会创建一个全局执行上下文并将其推入当前的执行栈。每当发生一个函数调用,引擎都会为该函数创建一个新的执行上下文并将其推到当前执行栈的顶端。
引擎会运行执行上下文在执行栈顶端的函数,当此函数运行完成后,其对应的执行上下文将会从执行栈中弹出,上下文控制权将移到当前执行栈的下一个执行上下文。
function first() { console.log('1'); second(); console.log('2'); } function second() { console.log('3'); } first(); console.log('4');
当上述代码在浏览器中加载时,JavaScript 引擎会创建一个全局执行上下文并且将它推入当前的执行栈。当调用 first()
函数时,JavaScript 引擎为该函数创建了一个新的执行上下文并将其推到当前执行栈的顶端。
当在 first()
函数中调用 second()
函数时,Javascript 引擎为该函数创建了一个新的执行上下文并将其推到当前执行栈的顶端。当 second()
函数执行完成后,它的执行上下文从当前执行栈中弹出,上下文控制权将移到当前执行栈的下一个执行上下文,即 first()
函数的执行上下文。
当 first()
函数执行完成后,它的执行上下文从当前执行栈中弹出,上下文控制权将移到全局执行上下文。一旦所有代码执行完毕,Javascript 引擎把全局执行上下文从执行栈中移除。
函数防抖
什么是函数防抖
概念:函数防抖(debounce),就是指触发事件后,在 n 秒内函数只能执行一次,如果触发事件后在 n 秒内又触发了事件,则会重新计算函数延执行时间。
例如:坐电梯的时候,如果电梯检测到有人进来(触发事件),就会多等待 10 秒,此时如果又有人进来(10秒之内重复触发事件),那么电梯就会再多等待 10 秒。在上述例子中,电梯在检测到有人进入 10 秒钟之后,才会关闭电梯门开始运行,因此,“函数防抖”的关键在于,在 一个事件 发生 一定时间 之后,才执行 特定动作。
为什么需要函数防抖
前端开发过程中,有一些事件,常见的例如,onresize,scroll,mousemove ,mousehover 等,会被频繁触发(短时间内多次触发),不做限制的话,有可能一秒之内执行几十次、几百次,如果在这些函数内部执行了其他函数,尤其是执行了操作 DOM 的函数(浏览器操作 DOM 是很耗费性能的),那不仅会浪费计算机资源,还会降低程序运行速度,甚至造成浏览器卡死、崩溃。这种问题显然是致命的。除此之外,短时间内重复的 ajax 调用不仅会造成数据关系的混乱,还会造成网络拥塞,增加服务器压力,显然这个问题也是需要解决的。
怎么做
函数防抖的要点,是需要一个 setTimeout 来辅助实现,延迟运行需要执行的代码。如果方法多次触发,则把上次记录的延迟执行代码用 clearTimeout 清掉,重新开始计时。若计时期间事件没有被重新触发,等延迟时间计时完毕,则执行目标代码。
function debounce(fn,duration){ let timer = null; return function(){ if(timer !== null){ clearTimeout(timer); } timer = setTimeout(fn,duration); } } let num = 0; function demo(){ span.innerHTML = num++; } window.addEventListener("resize",debounce(demo,1000));
函数节流
函数节流,throttle。节流的概念可以想象一下水坝,你建了水坝在河道中,不能让水流动不了,你只能让水流慢些。换言之,你不能让用户的方法都不执行。如果这样干,就是函数防抖debounce了。为了让用户的方法在某个时间段内只执行一次,我们需要保存上次执行的时间点与定时器。
也可以理解为:指连续触发事件但是在 n 秒中只执行一次函数,即 2n 秒内执行 2 次... 。节流如字面意思,会稀释函数的执行频率。
function throttle(func, wait) { let timeout; return function() { let _this = this; let args = arguments; if (!timeout) { timeout = setTimeout(() => { timeout = null; func.apply(_this, args) }, wait) } } } let num = 0; function demo() { span.innerHTML = num++; }; box.onmousemove = throttle(demo,1000);
函数节流会用在比input, keyup更频繁触发的事件中,如resize, touchmove, mousemove, scroll。throttle
会强制函数以固定的速率执行。因此这个方法比较适合应用于动画相关的场景。
总结:
-
防抖(debounce):对于短时间内连续触发的事件(上面的场景:连续请求同一个接口),防抖的含义就是让某个时间期限内(约定200毫秒),事情处理函数只执行一次。举个栗子:我连续输入100个字母,在输入最后一个字母后,再等200毫秒,执行请求接口。
-
节流(throttle):对于短时间内连续触发的事件(上面的场景:连续请求同一个接口),节流的含义就是在某个时间期限内每隔(预定300毫秒)执行一次。举个例子:我连续输入100个字母,在输入过程中,每隔300毫秒就请求一次接口。也就是说等输入完100个字母,会请求很多次接口。
debounce限制多长时间才能执行一次,throttle限制多长时间必须执行一次,一个限制上限、一个限制下限。
闭包
《JavaScript高级程序设计》这样描述:
闭包是指有权访问另一个函数作用域中的变量的函数。
在js中变量的作用域属于函数作用域, 在函数执行完后,作用域就会被清理,内存也会随之被回收,但是由于闭包函数是建立在函数内部的子函数, 由于其可访问上级作用域,即使上级函数执行完, 作用域也不会随之销毁, 这时的子函数(也就是闭包),便拥有了访问上级作用域中变量的权限,即使上级函数执行完后作用域内的值也不会被销毁。
代码如下:
var x = 10 function fn() { console.log(x) x -= 4 }
我们在同一作用域(全局作用域window)声明了变量x及函数fn,在函数中可以访问到函数外的x,不知不觉中便形成了一个最简单的闭包.
思考一个问题: 出于种种原因,我们有时候需要得到函数内的局部变量。那么如何从外部读取局部变量?
那么,上面的例子修改成:
function foo(){ var x = 10 function fn() { console.log(x) x -= 4 } return fn } var fn=foo(); // 得到 foo() 执行后返回的 fn 函数 fn() // 之后无论何时调用 fn,都还能访问到变量x
改后,x是一个局部变量,在foo()函数外部无法直接访问。只能通过fn()函数访问,我们把fn()函数作为foo()的返回值返回,以备后续调用。之后无论何时再执行fn(),都能访问到x,因此,虽然foo()函数执行完了,但x变量没有被释放.
由此可见,当函数跨作用域(子访问父)访问变量时,便会形成闭包,而这种“父函数嵌套子函数,子函数访问父函数中变量,再将子函数返回或者挂载到全局”的写法,也是常见的形成闭包的方式。
闭包的定义非常抽象,很难看懂。我的理解是:闭包就是能够读取其他函数内部变量的函数。由于在js语言中,只有函数内部的子函数才能读取局部变量,因此可以把闭包简单理解成"定义在一个函数内部的函数"。所以,在本质上,闭包就是将函数内部和函数外部连接起来的一座桥梁。
例如:写一个for循环,让它按顺序打印出当前循环次数
for (var i = 0; i < 5; i++) { setTimeout(() => { console.log(i) // 1s 后,打印 0 1 2 3 4 }, 1000) } // 由于js是单线程的,所以在执行for循环的时候定时器setTimeout被安排到任务队列中排队等待执行,而在等待过程中for循环就已经在执行,等到setTimeout可以执行的时候,for循环已经结束,i的值也已经编程5,所以打印出来五个5
for (var i = 0; i < 5; i++) { (function(i) { setTimeout(() => { console.log(i) // 1s 后,打印 0 1 2 3 4 }, 1000) })(i); } // 引入闭包来保存变量i,将setTimeout放入立即执行函数中,将for循环中的循环值i作为参数传递,1秒后同时打印出1 2 3 4 5
总结一下闭包的优点和缺点
优点:
-
可以读取函数内部的变量
-
变量的值始终保持在内存中
缺点:
-
由于闭包会使得函数中的变量都被保存在内存中,内存消耗很大,所以不能滥用闭包,否则会可能导致内存泄露,造成网页的性能问题。解决方法是,在退出函数之前,将不使用的局部变量全部删除。
-
闭包会在父函数外部,改变父函数内部变量的值。所以一定要小心,不要随便改变父函数内部变量的值。
-
什么是闭包
-
闭包的优缺点
扩展
function fun(n,o) { console.log(o) return { fun:function(m){ return fun(m,n); } }; } var a = fun(0); a.fun(1); a.fun(2); a.fun(3); var b = fun(0).fun(1).fun(2).fun(3); var c = fun(0).fun(1); c.fun(2); c.fun(3);
1、第一行a var a = fun(0); a.fun(1); a.fun(2); a.fun(3); 可以得知,第一个fun(0)是在调用第一层fun函数。第二个fun(1)是在调用前一个fun的返回值的fun函数,所以: 第后面几个fun(1),fun(2),fun(3),函数都是在调用第二层fun函数。 遂: 在第一次调用fun(0)时,o为undefined; 第二次调用fun(1)时m为1,此时fun闭包了外层函数的n,也就是第一次调用的n=0,即m=1,n=0,并在内部调用第一层fun函数fun(1,0);所以o为0; 第三次调用fun(2)时m为2,但依然是调用a.fun,所以还是闭包了第一次调用时的n,所以内部调用第一层的fun(2,0);所以o为0 第四次同理; 即:最终答案为undefined,0,0,0 2、第二行b var b = fun(0).fun(1).fun(2).fun(3);//undefined,?,?,? 先从fun(0)开始看,肯定是调用的第一层fun函数;而他的返回值是一个对象,所以第二个fun(1)调用的是第二层fun函数,后面几个也是调用的第二层fun函数。 遂: 在第一次调用第一层fun(0)时,o为undefined; 第二次调用 .fun(1)时m为1,此时fun闭包了外层函数的n,也就是第一次调用的n=0,即m=1,n=0,并在内部调用第一层fun函数fun(1,0);所以o为0; 第三次调用 .fun(2)时m为2,此时当前的fun函数不是第一次执行的返回对象,而是第二次执行的返回对象。而在第二次执行第一层fun函数时时(1,0)所以n=1,o=0,返回时闭包了第二次的n,遂在第三次调用第三层fun函数时m=2,n=1,即调用第一层fun函数fun(2,1),所以o为1; 第四次调用 .fun(3)时m为3,闭包了第三次调用的n,同理,最终调用第一层fun函数为fun(3,2);所以o为2; 即最终答案:undefined,0,1,2 3、第三行c var c = fun(0).fun(1); c.fun(2); c.fun(3);//undefined,?,?,? 根据前面两个例子,可以得知: fun(0)为执行第一层fun函数,.fun(1)执行的是fun(0)返回的第二层fun函数,这里语句结束,遂c存放的是fun(1)的返回值,而不是fun(0)的返回值,所以c中闭包的也是fun(1)第二次执行的n的值。c.fun(2)执行的是fun(1)返回的第二层fun函数,c.fun(3)执行的也是fun(1)返回的第二层fun函数。 遂: 在第一次调用第一层fun(0)时,o为undefined; 第二次调用 .fun(1)时m为1,此时fun闭包了外层函数的n,也就是第一次调用的n=0,即m=1,n=0,并在内部调用第一层fun函数fun(1,0);所以o为0; 第三次调用 .fun(2)时m为2,此时fun闭包的是第二次调用的n=1,即m=2,n=1,并在内部调用第一层fun函数fun(2,1);所以o为1; 第四次.fun(3)时同理,但依然是调用的第二次的返回值,遂最终调用第一层fun函数fun(3,1),所以o还为1 即最终答案:undefined,0,1,1