1、JS中的堆(Heap)栈(Stack)内存
都是在计算机内存中开辟的空间
- 栈内存 Stack:ECStack(Execution [ˌeksɪˈkjuːʃn] Context Stack)
- 存储原始值类型值
- 代码执行的环境
- 堆内存Heap
- 存储对象类型值
EC(Execution [ˌeksɪˈkjuːʃn] Context )执行上下文:区分代码执行的环境
- 常见的上下文分类:
- 全局上下文 EC(G)
- 函数私有上下文 EC(?)
- 块级私有上下文 EC(BLOCK)
- 产生私有上下文 -> 进栈执行 -> 出栈释放(可能释放)
- 变量对象:当前上下文中,用来存储声明变量的地方
- VO(Varibale Object):VO(G) 或者 VO(BLOCK)
- AO(Active Object):AO(?)
GO(Global Object)全局对象
- window指向GO对象
- 全局上下文中,基于var/function声明的变量是直接存储到GO对象上的,而基于let/const声明的变量才是存放在VO(G)中的
let 变量 = 值 的操作步骤
-
第一步:创建值
- 原始对象类型:直接存储在栈内存中,按值操作
- 对象类型值:按照堆内存地址来操作
- 对象:开辟一个堆内存空间(16进制地址)、一并存储对象的键值对、把空间地址赋值给变量
- 函数:内存空间中存储三部分信息
- 作用域[[scope]]:当前所处上下文
- 函数体中的代码字符串
- 当作普通对象存储的静态属性和方法[name&length]
-
第二步:声明变量 declare
-
第三步:变量和值关联在一起(赋值)defined
// console.log(a); //首先会到VO(G)查找,看是否为全局变量对象,如果不是,则再去GO中找,看是否为全局对象的一个属性,如果还不是,则报错 Uncaught ReferenceError: a is not defined
// console.log(window.a); //直接去GO中查找是否存在a这个成员,如果没有则不会报错,值是undefined
代码:
debugger; var x = 12; let y = 13; z = 14; //window.z=14; console.log(x, y, z); //12,13,14 console.log(window.x, window.y, window.z); //12 undefined 14
练习题:
let x = [12, 23]; const fn = function fn(y) { y[0] = 100; y = [100]; y[1] = 200; console.log(y); }; fn(x); console.log(x);
2、JS代码执行的预处理机制:变量提升
在”当前上下文“中,代码执行之前,浏览器首先会把所有带var/function关键字的进行提前声明或者定义:带var的只是提前声明&带function的,此阶段声明+定义(赋值)都完成了
-
let/const/import/class声明的变量不存在变量提升
-
重复声明的问题
-
推荐使用函数表达式,确保函数执行只能放在”创建函数“的下面,保证逻辑严谨性
-
const fn = function fn() {
console.log(1);
};
fn();
-
-
条件判断:在当前上下文中,变量提升阶段,不论条件是否成立,都要进行变量提升
- var:还是只声明
- function:判断体中的函数,在变量提升阶段,只声明不赋值
/* EC(G)全局执行上下文 VO(G)/GO a -> 12 变量提升:var a; 代码执行 */ // 其实最开始浏览器从服务器端获取的JS都是文本(字符串),只不过声明了其格式是「Content-Type: application/javascript;」,浏览器首先按照这个格式去解析代码 -> “词法解析”阶段「目标是生成“AST词法解析树”」 // 基于let/const等声明的变量:在词法解析阶段,其实就已经明确了,未来在此上下文中,必然会存在这些变量;代码执行中,如果出现在具体声明的代码之前使用这些变量,浏览器会抛出错误!! debugger; console.log(a); //undefined var a = 12; console.log(b); //Uncaught ReferenceError: Cannot access 'b' before initialization let b = 12; //==只有带var/function存在变量提升,带let/const的不存在,所以不能再声明之前使用这个变量「体现出ES6这版本的语法规范更加的严谨」
/* EC(G) VO(G)/GO fn -> 0x001 [[scope]]:EC(G) -> 0x002 [[scope]]:EC(G) -> 12 变量提升 function fn(){ console.log(1); } 声明+定义{赋值} var fn; 声明 function fn(){ console.log(2); } 声明+定义{赋值} */ console.log(fn); //0x002 function fn(){ console.log(1); } //变量提升阶段已经处理过了,直接跳过即可 console.log(fn); //0x002 var fn = 12; console.log(fn); //12 function fn(){ console.log(2); } //跳过 console.log(fn); //12
/* EC(G) VO(G)/GO a => window.a=undefined 变量提升: var a; //变量提升:不论条件是否成立,都要进行变量提升(对于var来讲新老版本浏览器没有任何影响,但是对于判断体中出现的function来讲,新老版本表现不一致:老版本 函数还是声明+定义 新版本 函数只会声明,不在定义) */ console.log(a); //undefined if (!('a' in window)) { // attr in obj:检测attr是否为obj对象的一个属性(成员),如果是对象的属性,结果是true // 'a' in window => true var a = 13; } console.log(a); //undefined
3、块级私有上下文
除”函数和对象“的大括号外 [例如:判断体/循环体/代码块...] ,如果在大括号中出现了let/const/function/class等关键词声明变量,则当前大括号会产生一个”块级私有上下文“;它的上级上下文是所处的环境;var不产生,也不受块级上下文的影响
- 函数是个渣男
- 循环中的块级上下文
//忽略报错的影响 console.log(a); // console.log(b); var a = 12; let b = 13; if (1 == 1) { console.log(a); // console.log(b); var a = 100; let b = 200; console.log(a); console.log(b); } console.log(a); console.log(b);
// 函数是个渣男 debugger; console.log(foo); if (1 === 1) { console.log(foo); function foo() {} foo = 1; console.log(foo); } console.log(foo);
//注意函数执行是没有返回值的
/* EC(G) VO(G) / GO f -> 0x000 [[scope]]:EC(G) “return true” g -> 0x001 [[scope]]:EC(G) “return false” 变量提升:- - */ f = function () {return true;}; g = function () {return false;}; (function () { /!* EC(AN) AO(AN) g 作用域链:<EC(AN),EC(G)> 初始THIS:window / undefined 初始ARG:... 形参赋值:-- 变量提升:function g; 只声明不定义了「因为其出现在判断体中」 *!/ if (g() && [] == ![]) { //Uncaught TypeError: g is not a function 报错,后面代码无法执行 f = function () {return false;} function g() {return true;} } })(); console.log(f()); console.log(g());
4、闭包作用域和浏览器垃圾回收机制
浏览器垃圾回收机制【GC】
-
标记清除
let obj1={name:'xxx'}
0x001 标记:是否被占用
let obj2=obj1;
obj2=null
obj1=null
0x001 不被占用
浏览器在空闲的时候 释放所有不被占用的内容
-
引用计数
let obj1={name:'zhufeng'};
0x000 计数1
let obj = obj1;
obj2 -> 0x000 计数2
obj2 = 10;
0x000 计数 1
浏览器空闲的时候 会把所有计数为1 释放掉
IE 浏览器 => 导致内存泄露(堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。)
全局上下文:打开页面,执行全局代码就会形成;只有当页面关闭的时候才会释放;
私有上下文:一般函数(代码块)中的代码执行完,浏览器会自动把私有上下文出栈释放;但是如果,当前上下文中,某个和它关联的内容(一般指的是一个堆内容)被当前上下文以外的事务占用了,那么这个私有上下文不能出栈释放;这样私有上下文中的“私有变量/值”也被保存起来了!
闭包的机制:保护、保存
闭包:
是一种机制,函数执行会产生一个私有上下文,这个上下文中的私有变量是受“保护”的,不会导致全局变量污染。
如果私有上下文不被释放,则这些私有变量和其对应的值也都被“保存”下来了,可以供其下级上下文中使用。
我们把上下文保护和保存的机制称为闭包。
let x = 5; function fn(x) { return function(y) { console.log(y + (++x)); } } let f = fn(6); f(7); fn(8)(9); f(10); console.log(x);
let a=0, b=0; function A(a){ A=function(b){ alert(a+b++); }; alert(a++); } A(1); A(2);
练习题
套娃:
function fun(n, o) { console.log(o); return { fun: function (m) { return fun(m, n); } }; } var c = fun(0).fun(1); c.fun(2); c.fun(3);
5、关于JS中THIS的基本情况分析
THIS:函数执行的主体(谁执行的函数)
- 事件绑定
- 函数执行【普通函数执行、成员访问、匿名函数、回调函数...】
- 构造函数
- 箭头函数【生成器函数generator】
- 基于call/apply/bind强制修改this指向
- ....
全局上下文中的this:window
块级上下文中没有自己的this,所有的this都是继承上级上下文中的this【箭头函数也是】
事件绑定
DOM0:xxx.oncxxx=function(){}
DOM2:
xxx.addEventListener('xxx',function()){}
xxx.attachEvent('onxxx',function(){})【ie6,7,8;现在基本不考虑兼容ie6,7,8】
给当前元素的某个事件行为绑定方法【此时是创建方法,方法没执行,当事件行为触发,浏览器会把绑定的函数执行,此时函数中的this->当前元素对象本身】
- 特殊:基于attachEvent实现事件绑定,方法执行,方法中的this是window
document.body.addEventListener('click',function(){
console.log(this);// ->body
})
函数执行
正常的普通函数执行:看函数执行前是否有“点”,有,“点”前面是谁this就是谁,没有“点”,this是window【严格模式下是undefined】
匿名函数:
- 函数表达式:等同于普通函数或者事件绑定等机制
- 自执行函数:this一般都是window/undefined
- 回调函数:一般都是window/undefined,但是如果另外函数执行中,对回调函数的执行做了特殊的处理,以函数自己处理的为主
括号表达式:小括号中包含“多项”,这样也只取最后一项,但是this受到影响(一般是window/undefined)
"use strict";//开启JS严格模式(默认是非严格模式)
function fn(){
console.log(this);//this -> window/undefined
}
let obj = {
name:"zhufeng",
//fn:fn;es6新语法
fn
}
fn();//this -> window/undefined
obj.fn();//this -> obj
(10,obj.fn());//this ->window/undefined
自执行函数
在堆中生成一个内存地址,并且执行会有一个函数私有上下文
(function(x){ console.log(this);// -> window/undefined })(10);
回调函数
回调函数:把一个函数A作为实参,传递给另外一个执行的函数B【在B函数执行中,可以把A执行】
function fn(callback){ //callback -> 匿名函数 callback(); } fn(fucntion(){ console.log(this);// this -> window/undefined })let arr = [10,20,30]; arr.forEach(function(item,index){ console.log(this);//this -> window }); arr.forEach(function(item,index){ console.log(this);//this -> {xxx:"xxx"} },{xxx:"xxx"});setTimeout(function(x){ console.log(this,x);//this -> window 10,多余的参数加在this后面 },1000,10)
案例:
var x = 3, obj = { x:5 }; obj.fn = (function(){ this.x *= ++x; return function(){ this.x *= (++x) + y; console.log(x); } })(); var fn = obj.fn; obj.fn(6); fn(4); console.log(obj.x, x);
6、专题总结:let/const/var 的区别
在所有的操作之前,首先进行的第一个操作”词法分析“,生成词法解析树 AST;词法解析树是给浏览器看的;
let和var的区别?
-
变量提升:let不存在变量提升,而var是具备变量提升的
-
重复声明:let不允许重复声明【当前上下文中,不论当前变量基于何种方式声明过,都不允许再用let声明了】,一旦重复声明,AST词法阶段都过不了,啥代码都不会执行;var不会重复声明,但是不会报错。
console.log(n); //Uncaught ReferenceError: Cannot access 'n' before initialization 词法分析阶段,我们就知道未来在全局上下文中会基于let声明一个n的变量,所以此时报错是:不允许在声明之前使用他...
console.log(m); //undefined
let n = 10;
var m = 20;
在词法解析阶段就会报错,后面代码不执行
// 在词法分析阶段报错 Uncaught SyntaxError: Identifier 'n' has already been declared
console.log('OK');
let n = 10;
var n = 20;
-
和GO的关系:在全局上下文中,基于var/function声明的变量是放在GO中的【可以基于window.xxx去访问、也可以直接获取】;但是基于let/const声明的变量是放在VO(G)中的,和GO没有关系
-
块级上下文:在除函数/对象的大括号外,如果括号中出现let/const/function/class会产生块级私有上下文,而且声明的变量也是块中的私有变量;但是var既不会产生块级上下文,块级上下文也不会对其产生影响
-
暂时性死区问题
// console.log(typeof n); //undefined 基于typeof检测一个未被声明的变量,结果不会报错,而是“undefined”
// console.log(typeof n); //Uncaught ReferenceError: Cannot access 'n' before initialization
// let n = 20;
let和const的区别?
-
let声明的是变量
-
const声明的是常量【不准确】;const声明的也是变量,只不过不允许重新关联其他的值(不允许指针重新指向)
const obj = {
name: 'zhufeng'
};
obj.name = '哈哈哈';
console.log(obj); //{name:'哈哈哈'}
const m = 20;
m = 30; //Uncaught TypeError: Assignment to constant variable. 不允许重新关联
const m; //Uncaught SyntaxError: Missing initializer in const declaration 必须赋值初始值
7、闭包的实际应用
闭包应用之:循环中的闭包处理方案
- 循环事件绑定
自定义属性
闭包的N中方案「含LET处理机制」
事件委托
- 循环中的定时器
- 闭包的处理方案
- 定时器本身处理方案
// 无法实现? 每次点击按钮,执行对应的方法,方法中的i不是私有的,而是全局的,而此时全局的i已经是循环结束的5了...
var btnList = document.querySelectorAll('.btn');
for (var i = 0; i < btnList.length; i++) {
btnList[i].onclick = function () {
console.log(`当前点击按钮的索引:${i}`);
};
}
// 解决办法一:闭包解决方案,利用闭包的“保存”机制
// 每一轮循环的时候,都创建一个闭包(不释放的上下文),闭包中存储自己的私有变量i,并且值是每一轮循环的索引;当点击按钮,执行对应的函数,遇到一个变量i,不要再去全局找了,而是让其去所属的闭包中查找即可...
// @1
var btnList = document.querySelectorAll('.btn');
for (var i = 0; i < btnList.length; i++) {
// 循环五次,产生五个不释放的闭包,每一个闭包中,都存在一个私有变量i,变量的值是对应的索引 0/1/2/3/4
(function (i) {
btnList[i].onclick = function () {
console.log(`当前点击按钮的索引:${i}`);
};
})(i);
}
// @2
var btnList = document.querySelectorAll('.btn');
for (var i = 0; i < btnList.length; i++) {
btnList[i].onclick = (function (i) {
return function () {
console.log(`当前点击按钮的索引:${i}`);
};
})(i);
}
// @3 NodeList集合本身具备forEach方法,和数组中的类似,都是用来迭代集合中每一项的
var btnList = document.querySelectorAll('.btn');
btnList.forEach(function (item, index) {
// 迭代集合中每一项,都把这个回调函数执行,产生一个闭包「因为上下文中创建的小函数,被外层的按钮对象的onclick占用了;每个闭包中有一个私有变量index,存储的是当前这一项的索引」
item.onclick = function () {
console.log(`当前点击按钮的索引:${index}`);
};
});
// @4 也是基于闭包的方案,只不过利用的是LET会产生块级上下文
let btnList = document.querySelectorAll('.btn');
for (let i = 0; i < btnList.length; i++) {
// 每一轮循环都产生一个私有的块级上下文,里面的内容(函数)被外部占用,也会产生一个闭包;而且每个闭包中,都有一个私有变量i记录索引
btnList[i].onclick = function () {
console.log(`当前点击按钮的索引:${i}`);
};
}
// 闭包方案虽然可以解决,但是比较消耗内存
//======================
// 解决方案二:自定义属性
let btnList = document.querySelectorAll('.btn');
let i = 0;
for (; i < btnList.length; i++) {
// 最开始每轮循环的时候,给每一个按钮对象都设定一个自定义属性myIndex,存储它的索引
btnList[i].myIndex = i;
btnList[i].onclick = function () {
// 每一次点击的时候,基于THIS(当前操作元素)获取之前存放的自定义属性值
console.log(`当前点击按钮的索引:${this.myIndex}`);
};
}
// 性能比闭包要好一些,但是也有一些性能消耗{元素对象 & 节点集合 & 绑定的方法 都是开辟的堆内存}
//======================
// 解决方案三:终极方案 事件委托
// 点击每一个按钮,除了触发按钮的点击事件行为,根据冒泡传播机制,也会把body的点击事件行为触发
// event 时间对象
document.body.onclick = function (ev) {
let target = ev.target;
if (target.tagName === 'BUTTON' && target.className === "btn") {
// 点击的事件源是按钮
let index = target.getAttribute('data-index');//data-index是自定义的class类
console.log(`当前点击按钮的索引:${index}`);
}
};
// 能否实现每间隔1秒输出 0 1 2?
for (var i = 0; i < 3; i++) {
setTimeout(function () {
console.log(i);
}, i * 1000); //等待时间 0ms(5~7ms) 1000ms 2000ms
}
// 现在效果是,每间隔1000ms都输出的是3? 定时器执行,方法中的i不是私有的,向上级找就是全局的i「此时全局i已经是循环结束的3」
// @1
for (let i = 0; i < 3; i++) {
setTimeout(function () {
console.log(i);
}, i * 1000);
}
// @2
let i = 0;
for (; i < 3; i++) {
(function (i) {
setTimeout(function () {
console.log(i);
}, i * 1000);
})(i);
}
// @3
const fn = i => {
return function () {
console.log(i);
};
};
let i = 0;
for (; i < 3; i++) {
setTimeout(fn(i), i * 1000);
}
// @4
let i = 0;
for (; i < 3; i++) {
// 设置定时器:
// 参数1:回调函数,到时间执行的方案
// 参数2:等待时间
// 参数3:给回调函数“预先传递”的实参值{底层本质也是闭包 柯理化函数思想}
setTimeout(function (n) {
console.log(n);
}, i * 1000, i);
}
8、匿名函数具名化
匿名函数具名化「符合官方语法规范,推荐使用」:原本匿名函数没有名字,我们给其设置一个名字
@1 我们给匿名函数设置的名字,并不会在其所处的上下文中声明,所以在函数外面不可以使用这个名字进行相关的操作
@2 但是会在自己执行产生的私有上下文中声明这个函数名,存储的值是当前这个匿名函数本身
@3 并且默认是不支持把其值进行修改的「改了也没用」
@4 但是如果当前私有上下文中,也有基于其余的方式声明过这个名字,则以自己声明的为主「也就是@2这一步做的事情优先级最低」
作用:可以实现匿名函数的递归处理、语法更规范、也无需担心会和外面的变量产生冲突...
箭头函数是匿名函数「无法设置名字,变量接收可以理解为是它的名字」
const fn = () => {};函数表达式一般是匿名函数
const fn = function () {}; document.onclick = function () {}; function fn() { return function () {}; }回调函数一般也是匿名函数
setTimeout(function () {}, 1000); fn(function () {});自执行函数一般也是匿名函数
(function(){ })();var fn = function sum() { console.log(sum); //具名化的名字可以在函数内部上下文中使用,代表当前函数本身 }; // console.log(sum); //Uncaught ReferenceError: sum is not defined 匿名函数具名化后的这个名字,在所处上下文中未被声明过 fn(); //---------------------------------- (function sum() { sum = 1; console.log(sum); //=>函数 具名化的名字在函数内部是不允许被修改值的 })(); //---------------------------------- (function sum() { // 具名化的名字权重比较低,但凡当前私有上下文中存在一个同名的私有变量,都以私有变量为主,不再是这个函数 console.log(sum); //=>Uncaught ReferenceError: Cannot access 'sum' before initialization let sum = 1; console.log(sum); //=>1 })(); //---------------------------------- // 作用:方便匿名函数递归处理,而且更符合规范 "use strict"; (function () { console.log(arguments.callee); //获取的是当前函数本身,但是在JS严格模式下,不允许使用callee「Uncaught TypeError: 'caller', 'callee', and 'arguments' properties may not be accessed on strict mode functions or the arguments objects for calls to them」 })(); //---------------------------------- "use strict"; let i = 0; (function sum() { i++; console.log(i); if (i < 2) { sum(); } })(); //---------------------------------- "use strict"; let total = (function anonymous(num) { // console.log(arguments.callee); //存储当前函数本身,但是严格迷失下Uncaught TypeError: 'caller', 'callee', and 'arguments'properties may not be accessed on strict mode functions or the arguments objects for calls to them if (num === 1) return num; return num + anonymous(num - 1); })(100); console.log(total); //---------------------------------- var b = 10; (function b() { b = 20; console.log(b); //函数 })(); console.log(b); //10
9、JS高阶编程技巧:模块化编程
模块化编程进化史
前端开发需遵循模块化编程:公用性&复用性、提高开发效率、方便管理、团队协作开发...
- 单例设计模式
- AMD [require.js]
- CommonJS
- CMD [sea.js]
- ES6Module
单例设计模式
利用对象 [单独堆内存] 来进行分组管理,避免全局变量污染
这种方案其实就是“单例设计模式”:每一个对象都是Object的单独实例,基于每一个实例对象来管理自己的属性和方法,实现分组的效果
person1/person2:namespace 命名空间
把描述同一个事务的属性和方法放在相同的命名空间中,以此来避免全局变量污染
let person1 = { name: '王宇浩', age: 42, friend: true, eat() {} } let person2 = { name: '闫闪闪', age: 18, friend: false, eat() {} } --------------------------------- //利用闭包的思想[单独的执行上下文]来进行分组管理,避免全局变量污染 (function() { let name = '王宇浩'; let age = 42; let friend = true; const query = () => {}; // 暴露API:挂载到GO中「不宜挂载过多,因为挂载过多也会冲突」 window.query = query; })(); (function() { let name = '闫闪闪'; let age = 18; let friend = false; query(); })(); --------------------------------- let AModule = (function() { let n = 10; const query = () => {}; const sum = () => {}; //暴漏API return { query } })(); let BModule = (function() { let n = 20; const sum = () => {}; AModule.query(); //暴漏API return {} })();//A模块 let AModule = (function () { let name = "珠峰"; const sum = function sum(...params) { let len = params.length; if (len === 0) return 0; if (len === 1) return +params[0]; return params.reduce((x, item) => (+x) + (+item)); }; return { sum }; })(); // B模块中需要依赖A模块中的sum方法 let BModule = (function () { let name = "培训"; const average = function average(...params) { let len = params.length, total = AModule.sum(...params); if (len === 0) return 0; return (total / len).toFixed(2); }; return { average }; })(); // 调用A/B模块中的方法实现对应的需求 console.log(AModule.sum(10, 20, 30, 40, 50)); console.log(BModule.average(10, 20, 30, 40, 50));<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>珠峰在线Web高级正式课「为大厂而生」</title> </head> <body> <!-- IMPORT JS --> <script src="A.js"></script> <script src="B.js"></script> <script src="main.js"></script> </body> </html> <!-- 最开始的模块化开发:把每个模块代码写在不同的文件中,最后在页面中分别导入 + 需要自己“手动分析出相关的依赖”,规划出导入的先后顺序「麻烦」;即便基于grunt/gulp/webpack等处理,也需要知道依赖关系,按照依赖顺序打包... + 如果不基于闭包把每个模块中的代码私有化处理,最后合并在一起的时候,容易引发“全局变量污染” + ... 解决私有化:自执行函数执行,产生闭包即可 解决模块之间的相互访问: + 把需要供外面访问的内容,暴露到全局上 window.xxx=xxx,但这种方式在需要暴露更多方法的时候,也可能会导致全局变量的冲突!! + 把模块中需要暴露的属性方法放在一个对象中管理,最后基于模块名存储这个对象即可 let xModule=(function(){ ... return { //包含了需要暴露给外面用的属性方法 fn, }; })(); 访问: xModule.fn() 总结:这种处理方案,即保证了模块代码间的私有化,也支持模块间的相互访问,而且避免了全局变量的污染... 我们把这种代码设计方法称之为“单例设计模式”;所有设计模式其实都是一种思想,这种思想解决了某一类问题!! -->单例设计模式需要自己写代码来管理;并且如果每一个模块是一个单独的JS,最后导入JS的时候,我们需要非常认真的去管理一下先后导入的顺序「按照模块之间的依赖去处理」;
AMD模块化思想 - require.js
//lib/A.js // define:定义模块 define(function () { let name = "珠峰"; const sum = function sum(...params) { let len = params.length; if (len === 0) return 0; if (len === 1) return +params[0]; return params.reduce((x, item) => (+x) + (+item)); }; return { sum }; }); //lib/B.js // AMD思想的优势:定义模块的时候,可以把依赖的模块“前置导入” // 回调函数中基于AModule接收导入的A模块内容(A模块中返回的对象) define(['A'], function (AModule) { let name = "培训"; const average = function average(...params) { let len = params.length, total = AModule.sum(...params); if (len === 0) return 0; return (total / len).toFixed(2); }; return { average }; }); //main.js require.config({ baseUrl: './lib' }); // 导入指定模块,然后处理相关的内容 require(['B', 'A'], function (B, A) { console.log(A.sum(10, 20, 30, 40, 50)); console.log(B.average(10, 20, 30, 40, 50)); });
main.js
require.config({ baseUrl: 'js/lib', }); require(['moduleB', 'moduleA'], function (moduleB, moduleA) { console.log(moduleB.average(10, 20, 30, 40, 50)); });moudleA.js
define(function () { return { // 任意数求和 sum(...args) { let len = args.length, firstItem = args[0]; if (len === 0) return 0; if (len === 1) return firstItem; return args.reduce((total, item) => { return total + item; }); } }; });moudleB.js
define(['moduleA'], function (moudleA) { return { // 求平均数(去掉最大最小值) average(...args) { let len = args.length, firstItem = args[0]; if (len === 0) return 0; if (len === 1) return firstItem; args.sort((a, b) => a - b); args.pop(); args.shift(); return (moudleA.sum(...args) / args.length).toFixed(2); } }; });自己实现一套AMD模块机制
let factories = {}; function define(moduleName, factory) { factories[moduleName] = factory; } function require(modules, callback) { modules = modules.map(function (item) { let factory = factories[item]; return factory(); }); callback(...modules); } /* 使用AMD */ define('moduleA', function () { return { fn() { console.log('moduleA'); } }; }); define('moduleB', function () { return { fn() { console.log('moduleB'); } }; }); require(['moduleA', 'moduleB'], function (moduleA, moduleB) { moduleB.fn(); moduleA.fn(); });
Common.js
CommonJS规范「只能在Node环境下运行;随用随导入,无需依赖前置」
导入:require
导出:module.exports
A.js
let name = "珠峰"; const sum = function sum(...params) { let len = params.length; if (len === 0) return 0; if (len === 1) return +params[0]; return params.reduce((x, item) => (+x) + (+item)); }; module.exports = { sum };B.js
const A = require('./A'); let name = "培训"; const average = function average(...params) { let len = params.length, total = A.sum(...params); if (len === 0) return 0; return (total / len).toFixed(2); }; module.exports = { average };main.js
/* CommonJS模块规范「模块的导入和导出」:Node自带的模块规范(浏览器端不支持) 定义模块:创建的每一个JS文件,就是定义一个单独的模块 导出模块中的方法: module.exports = { //包含需要供外部调用的属性和方法 }; 导入指定的模块: const x = require('模块地址,导入自己的模块需要加“./”'); 基于x接收导出的对象,后期基于 x.xxx 即可访问!! CommonJS模块的导入是“按需”的,随时用随时导入即可,不像AMD都需要前置处理!! */ const A = require('./A'); console.log(A.sum(10, 20, 30, 40, 50)); const { average } = require('./B'); console.log(average(10, 20, 30, 40, 50));
CMD[sea.js]
淘宝玉伯研发了一个插件:seajs,旨在把CommonJS规范搬到浏览器端运行,起了个规范名字“CMD”
ES6Module
ECMA官方出来个模块规范:ES6Module
- 导出:export & export default
- 导入:import
- 依赖前置;浏览器可以直接支持;NodeJS环境是不支持的;
使用时需要在添加 script 添加type="module"
<body> <!-- type="module":让浏览器支持ES6Module规范 页面需要基于标准的HTTP/HTTPS协议预览,不能是file协议「vscode:Live Server」 --> <script type="module" src="main.js"></script> </body>A.js
const sum = function sum() { console.log('A SUM'); }; const fn = function fn() { console.log('A FN'); }; /* // 一个个的导出,并且导出多个 // 导入的时候 import * as TYPE from './A.js'; // TYPE.n / TYPE.m export const n = 10; export const m = 20; */ export default { sum, fn };B.js
const query = function query() { console.log('B QUERY'); }; export default query;main.js
// 导入必须放在最开始 import { fn, sum } from './A.js'; import query from './B.js'; // A.fn(); // A.sum(); query();export/export default
- export default 向外暴露的成员,可以使用任意变量来接收
- 在一个模块中,export default 只允许向外暴露一次
- 在一个模块中,可以同时使用export default 和export 向外暴露成员
- 使用export向外暴露的成员,只能使用{ }的形式来接收,这种形式,叫做【按需导出】
- export default 可以向外暴露多个成员,同时,如果某些成员,在import导入时,不需要,可以不在{ }中定义
- 使用export导出的成员,必须严格按照导出时候的名称,来使用{ }按需接收
- 使用export导出的成员,如果想换个变量名称接收,可以使用as来起别名
<!-- 在浏览器端开启ES6Module规范 + type="module" + 基于标准的http/https协议的web服务预览页面 定义模块:和CommonJS类似,创建一个JS就相当于创建一个模块 导出/导入模块: export 声明变量且赋值; export default 值; -> 在一个模块中只能使用一次 每个模块导出一个Module对象 { num:10, ..., default:sum } import x from '模块地址'; -> 浏览器端直接使用,地址中模块的后缀不能省略 -> 只能接收到基于 export default 导出的这个值 -> 原理:找到导出Module对象中的default属性值,把属性值赋值给x变量 -> 但是不能在这直接给x解构赋值 ,例如:import {n,m} from '模块地址'; 这样是不能给default后面的值解构赋值;需要解构赋值,则先基于x接收,然后再给x解构赋值即可;例如:const {n,m}=x; import * as x from '模块地址'; 把当前模块导出的所有内容获取到,赋值给x变量,后期基于 x.xxx 访问即可「含: x.default 获取export default导出的值」 import { num, obj } from '模块地址'; 直接结构赋值,是把模块导出的Module中所有内容(不含default)进行解构赋值 import需要放在模块代码的最上面编写,有点类似于前置导入 --> // test.js var info = { name: 'zs', age: 20 } export default info export var title = '小星星' export var content = '哈哈哈'import person, {title, content as content1} from './test.js' console.log(person); console.log(title + '=======' + content1);
总结
单例设计模式是“最早期的模块规范”,在没有CommonJS/ES6Module模块规范的时代,帮助我们实现了模块化开发! AMD(require.js)是在单例设计模式的基础上,实现了模块和模块之间的依赖管理! -----但是上述操作都是过去时了
当代前端开发,都是基于模块化进行开发,而模块化方案以 CommonJS/ES6Module 为主
- 他们都是按照创建一个JS就是创建一个模块来管理的「每个JS文件中的代码都是私有的」
- CommonJS:require && module.exports
- ES6Module:export && import
CommonJS规范比AMD用起来更简单,从导入机制等原理上,也比AMD性能高一些;但是CommonJS不支持浏览器端,所以 淘宝玉伯 写了一个插件 sea.js. 「把其定义为CMD模块规范」本质:把CommonJS规范搬到浏览器端运行;再到后来 ES6本身就提供了更好用的模块规范:ES6Module,sea.js代表的CMD规范就被pass掉了
我们编写的JS代码,可以运行的环境 @1 浏览器 <script src='...'> 「和其类似的还有webview」
+ 直接支持ES6Module,但是不支持CommonJS + 全局对象 window@2 NODE + 支持CommonJS,但是不支持ES6Module + 全局对象 global
@3 webpack「基于node实现代码的合并压缩打包、最后把打包的结果导入到浏览器中运行」 + CommonJS&ES6Module都支持,而且支持相互之间的“混用”(原理:webpack把两种模块规范都实现了一遍) + 支持 window&global
@4 vite「新的工程化打包工具」 + 不是像webpack一样编译打包的,它本质就是基于ES6Module规范,实现模块之间的相互引用
10、JS高阶编程技巧:JQ源码分析【环境处理】
JS代码可以在哪里执行
- 浏览器端 webkit(blink)、gecko、trident...
- webview[手机App中] webkit
- 有window,不支持CommonJS规范,支持ES6Module规范
- node环境
- 没有window、支持CommonJS规范、但是不支持ES6Module规范
- 可以基于webpack进行编译
- 支持window、也支持CommonJS规范、支持ES6Module规范(可以让ES6Module和CommonJS混合调用)...:基于node环境进行打包处理,打包后的结果交给浏览器端去渲染和运行
JQ整体框架
(function (global, factory) {
/*
* global:window(浏览器&webpack) / global(node)
* factory:回调函数
*/
"use strict";
if (typeof module === "object" && typeof module.exports === "object") {
// 当前运行的环境支持 CommonJS 模块规范「node & webpack」
// webpack环境下
// module.exports = factory(window, true)
// Node环境下「不支持JQ的使用」
// module.exports = function (w) {...}
// 使用 let $ = require('jquery'); -> $() -> 报错
module.exports = global.document ?
factory(global, true) :
function (w) {
if (!w.document) {
throw new Error("jQuery requires a window with a document");
}
return factory(w);
};
} else {
// 当前环境不支持CommonJS规范「浏览器环境」
factory(global);
}
})(
typeof window !== "undefined" ? window : this,
function (window, noGlobal) {
"use strict";
// 浏览器环境下导入JQ(<script src='js/jquery.min.js'>):window->window noGlobal->undefined
// webpack环境下运行:window->window noGlobal->true 把factory执行的返回值导出
var version = "3.6.0",
jQuery = function (selector, context) {
// ...
};
/* 暴露API */
// 当前环境下支持AMD模块思想(导入了require.min.js),此时我们基于AMD思想定义JQ模块
// 使用:require(['jquery'],function($){ $(); });
if (typeof define === "function" && define.amd) {
define("jquery", [], function () {
return jQuery;
});
}
// 浏览器中直接导入运行 使用:$()或者jQuery()
if (typeof noGlobal === "undefined") {
window.jQuery = window.$ = jQuery;
}
// 在webpack环境下运行:module.exports = jQuery;
// 使用:const $ = require('jquery'); -> $();
return jQuery;
}
);
简单写法:
/* 封装一些项目中常用的工具方法,类似于lodash/underscore...这些库,支持各种环境下运行 */ // 简便的写法 (function () { "use strict"; let utils = { version: '1.0.0' }; /* 暴露API */ //支持CommonJS规范,在NODE或WEBPACK端运行 if (typeof module === "object" && typeof module.exports === "object") module.exports = utils; //在浏览器端运行 if (typeof window !== "undefined") window.utils = utils; })(); // 参考JQ的写法 /* (function (global, factory) { "use strict"; if (typeof module === "object" && typeof module.exports === "object") { module.exports = factory(global, true); } else { factory(global); } })(typeof window !== "undefined" ? window : this, function factory(window, noGlobal) { "use strict"; let version = '1.0.0', utils = { version }; /!* 暴露API *!/ if (typeof define === "function" && define.amd) { define("utils", [], function () { return utils; }); } if (typeof noGlobal === "undefined") window.utils = utils; return utils; }); */
11、JS高阶编程技巧:柯里化函数编程思想
闭包:保存 & 保护
柯里化思想:编程思想,函数执行产生一个“闭包”(不被释放的上下文),把一些信息“预先存储”起来,目的是供其下级上下文中调取使用;这样预习存储和处理的思想,就叫做柯理化函数编程思想;
作用域 VS 上下文
都是函数执行,在栈内存中分配出来的空间;创建函数时候,在哪个上下文中创建的,那么其作用域就是谁(作用域不是函数自己执行产生的这个空间,而是创建函数所在的这个空间);而上下文是函数自己执行产生的!! => 函数执行产生的私有上下文,它的上级上下文是它的作用域!!
柯里化函数编程思想应用
const fn = function fn(...params) { //params:[1,2] return function proxy(...args) { //args:[3] params = params.concat(args); return params.reduce(function(result, item) { return result + item; }) } } //箭头函数->//const fn = (...params) => (...args) => params.concat(args).reduce((result, item) => result + item) let res = fn(1, 2)(3); console.log(res); //=>6 1+2+3
// 需求:add一直持续执行,执行几次是不确定的,我们最后需要把每一次执行传递的值累加求和!! // 浏览器没有升级之前,基于 console.log(函数) ,会把函数转换为字符串输出,函数[Symbol.toPrimitive] -> 函数.valueOf() -> 函数.toString(),但凡其中有一项返回了对应的值,则控制台以返回值输出为主;但是升级后,转换为字符串的操作流程还会触发,但是控制台最后呈现的依然是函数! const curring = function curring() { let result = []; const add = (...params) => { // params:数组,接收每一次add执行传递的实参 // 把每一次传递进来的值,都存储到result容器中 result = result.concat(params); return add; }; add[Symbol.toPrimitive] = () => { return result.reduce((x, item) => x + item);//返回结果 }; return add; }; let add = curring(); console.log(+add(1)(2)(3)(4)(5)); add = curring(); console.log(+add(1, 2)(3, 4)(5)); add = curring(); console.log(+add(1, 2, 3, 4, 5)); //==========最原始的面试题,执行curring函数需要指定执行的次数的 /* const curring = function curring(count) { let params = [], n = 0; const add = (...args) => { params = params.concat(args); n++; if (n >= count) { // 求和 return params.reduce((result, item) => result + item); } return add; }; return add; }; let add = curring(3); let res = add(1)(2)(3); console.log(res); //->6 add = curring(2); res = add(1, 2, 3)(4); console.log(res); //->10 add = curring(5); res = add(1)(2)(3)(4)(5); console.log(res); //->15 */
const fn = function fn(x, y) { return x + y; }; fn[Symbol.toPrimitive] = function () { return 10; }; // alert(fn); //先把fn变为字符串,然后再输出 String(fn) : Symbol.toPrimitive->valueOf->toString // console.log(fn); //也是要把fn变为字符串再输出的,控制台会在字符串前面加一个 ƒ,代表这是函数字符串 「新版谷歌浏览器修改了机制:log输出也会执行Symbol.toPrimitive等方法,但是不论方法最后返回啥,还是输出的函数字符串」 */
函数式编程 VS 命令式编程
函数式编程:WHAT 把具体执行的步骤封装到一个函数中,后期需要处理的时候,只需要把函数执行即可;我们不再关注执行的步骤,只关注最后处理的结果;
低耦合高内聚
快捷化开发、方便维护
不能灵活掌控程序处理的步骤,无法在某一步骤做些特殊处理...
let arr = [10, 20, 30, 40]; Array.prototype.forEach = function forEach(callback) { let self = this; for (let i = 0; i < self.length; i++) { callback(self[i], i); } }; // forEach就是函数式编程:函数内部实现了对数组迭代的封装,每一次迭代都把回调函数执行,并且把当前迭代这一项及其索引传递过来!! arr.forEach((item, index) => { console.log(item, index); });命令式编程:HOW 更关注处理的步骤,需要我们自己去实现每一步的操作
灵活,想咋处理咋处理
代码冗余度高、开发效率慢...
/ 自己写循环就是命令式编程 for (let i = 0; i < arr.length; i++) { console.log(arr[i], i); }真实项目中推荐使用函数式编程
for循环和forEach的区别?
参考答案:
for循环代表的是命令是编程、forEach代表的是函数式编程;
forEach其实就是把数组迭代的操作步骤封装好,这样应用起来会更加方便;我之前研究过forEach等数组常见方法的源码,forEach内部是依次迭代数组每一项,每一次迭代把传递的回调函数执行,把迭代的内容及索引传递给回调函数....直到整个数组都迭代完毕才结束,不支持中间以任何形式跳过或者结束迭代操作!!而for循环是命令式编程,所有的操作步骤自己可以管控,想啥时候结束就结束,想咋循环就咋循环;
我在项目开发的时候,一般应用的都是forEach,这样可以提高我的开发效率,减少代码的冗余!!但是遇到一些需要灵活迭代的需求,则自己基于for循环操作!!
Array.prototype.reduce
基于JS重写Array.prototype.reduce函数
callback
- 执行数组中每个值 (如果没有提供 initialValue 则第一个值除外)的函数,包含四个参数:
- accumulator - callback 累计器累计回调的返回值; 它是上一次调用回调时返回的累积值,或 initialValue(见于下方)。
- element - callback 数组中当前正在处理的元素。
- index - 可选,数组中正在处理的当前元素的索引。 如果提供了 initialValue,则起始索引号为 0,否则从索引 1 起始。
- array - 可选,reduce 方法调用的数组
initialValue - 可选,作为第一次调用 callback 函数时的第一个参数的值。 如果没有提供初始值,则将使用数组中的第一个元素。 在没有初始值的空数组上调用 reduce 将报错。
// 迭代数据中的每一项,而且可以获取上一次迭代处理的结果「回调函数中的返回值」,再继续处理,实现处理结果的累积操作!! // let arr = [10, 20, 30, 40, 50]; /* let result = arr.reduce((x, item, index) => { // x初始值是数组的第一项,从数组第二项开始迭代 // 第一次 x=10 item=20 index=1 返回30 // 第二次 x=30 item=30 index=2 返回60 // 第三次 x=60 item=40 index=3 返回100 // 第四次 x=100 item=50 index=4 返回150 // 迭代结束,最后一次返回的150赋值给外面的result return x + item; }); */ /* let result = arr.reduce((x, item, index) => { // x初始值是传递的第二个参数,从数组第一项开始迭代 // 第一次 x=0 item=10 index=0 返回10 // ... return x + item; }, 0); */ arr.reduce(callback(accumulator, currentValue[, index[, array]])[, initialValue])// 面试题:重写reduce方法 Array.prototype.my_reduce = function reduce(callBack, initValue) { if (typeof callBack !== "function") { throw new Error(`${callBack} is not a function`); } //获取当前的数组 const ary = this; //判读是否设置了初始值initValue,如果有则遍历数组从0开始,如果没有则需要把数组的第一位拿出来当初始值,遍历数组就要从1开始 let startIndex = initValue ? 0 : 1; let result = initValue ? initValue : ary[0]; for (let i = startIndex; i < ary.length; i++) { result = callBack(result, ary[i], i, ary); } return result; }; let array = [10, 20, 30]; let res = array.my_reduce((pre, item) => { return pre + item; }) console.log(res);
/*
在函数式编程当中有一个很重要的概念就是函数组合, 实际上就是把处理数据的函数像管道一样连接起来, 然后让数据穿过管道得到最终的结果。 例如:
const add1 = (x) => x + 1;
const mul3 = (x) => x * 3;
const div2 = (x) => x / 2;
div2(mul3(add1(add1(0)))); //=>3
而这样的写法可读性明显太差了,我们可以构建一个compose函数,它接受任意多个函数作为参数(这些函数都只接受一个参数),然后compose返回的也是一个函数,达到以下的效果:
const operate = compose(add1,add1,mul3,div2)
operate(0) //=>相当于div2(mul3(add1(add1(0))))
operate(2) //=>相当于div2(mul3(add1(add1(2))))
简而言之:compose可以把类似于f(g(h(x)))这种写法简化成compose(f, g, h)(x),请你完成 compose函数的编写
*/
const add1 = x => x + 1;
const mul3 = x => x * 3;
const div2 = x => x / 2;
const compose = function(...funcs) {
let len = funcs.length;
if (len === 0) return x => x;
if (len === 1) return funcs[0];
return function operate(x) {
return funcs.reduce((result, item) => {
return typeof item === "function" ? item(result) : result;
},x)
}
}
/* // redux源码中提供的compose函数
const compose = function compose(...funcs) {
if (funcs.length === 0) return arg => arg;
if (funcs.length === 1) return funcs[0];
// funcs:[div2, mul3, add1, add1]
// a:div2 b:mul3 -> x=>div2(mul3(x))
// a:x=>div2(mul3(x)) b:add1 -> x=>a(add1(x)) -> x=>div2(mul3(add1(x)))
// a:x=>div2(mul3(add1(x))) b:add1 -> x=>a(add1(x)) -> x=>div2(mul3(add1(add1(x))))
return funcs.reduce((a, b) => {
return x => {
return a(b(x));
};
});
}; */
console.log(compose(div2, mul3, add1, add1)(0)); //=>2
console.log(compose()(0)); //=>0
console.log(compose(add1)(0)); //=>1
12、JS高阶编程技巧:惰性思想
getComputedStyle([element],[?伪类])
// window.getComputedStyle([element],[?伪类]) //获取当前元素所有经过浏览器计算的样式对象「但凡这个元素经过浏览器渲染了,所有的样式都可以获取到{含:自己写的、浏览器默认的...}」 // getComputedStyle(box).width // 不兼容IE6~8,在低版本浏览器中,需要使用 box.currentStyle.width const css = function css(elem, attr) { if ('getComputedStyle' in window) { // in 检测某个属性是不是对象的属性「不论私有还是公有」 return getComputedStyle(elem)[attr]; } return elem.currentStyle[attr]; };
基于函数重构实现惰性思想(减少逻辑处理)
/* var getCss = function (elem, attr) {
if (window.getComputedStyle) {
return window.getComputedStyle(elem)[attr];
}
return elem.currentStyle[attr];
};
console.log(getCss(document.body, 'margin'));
console.log(getCss(document.body, 'padding')); //瑕疵:浏览器没换、页面没关,第一次执行需要判断兼容性是必须的,但是第二次及以后再执行,如果还要判断兼容性,是没有必要的... */
function getCss(element, attr) {
if (window.getComputedStyle) {
getCss = function(element, attr) {
return window.getComputedStyle(element)[attr];
}
} else {
getCss = function(element, attr) {
return window.currentStyle(element)[attr];
}
}
}
console.log(getCss(document.body, 'width'));
console.log(getCss(document.body, 'margin'));
console.log(getCss(document.body, 'padding'));
13、JS高阶编程技巧:函数的防抖和节流
防抖:防止“老年帕金森”,在用户频繁进行某项操作的时候,我们只识别一次「自定义频繁的规则、自定义触发边界...」 节流:“降频”,在用户频繁进行某项操作的时候,我们降低默认的触发频率 备注:频繁的频率自己来设定
防抖
防抖的一种简单办法「具体的需求:频繁点击,只有当上一个请求成功,才能发送下一个请求」
运用场景:
- 登录、发短信等按钮避免用户点击太快,以致于发送了多次请求,需要防抖
- 调整浏览器窗口大小时,resize 次数过于频繁,造成计算过多,此时需要一次到位,就用到了防抖
- 文本编辑器实时保存,当无任何更改操作一秒后进行保存
防抖:
规则:500ms内触发两次及以上,就算频繁操作
我接下来一直点5min,只识别一次,因为我属于频繁操作,我一直点,过了3min后我不点了「在最后一次点击的500ms内没有点」,识别一次
过了1s钟,我们又开始频繁点 这是下一次频繁操作,还可以在识别一次
节流:控制触发的频率 一直点击5min,这样它会每间隔500ms触发一次
触发多次
业务使用:
const submit = document.getElementById('submit'); let runing = false; submit.onclick = function () { if (runing) return; runing = true; submit.disabled = true; console.log('click start'); query(val => { console.log('click end', val); runing = false; submit.disabled = false; }); };
第一次单击:设置个定时器,等待300ms
如果300ms没有点击第二次,属于非频繁触发,我们让handle执行一次
如果触发了第二次,属于频繁操作,首先我们上一个没到时间的定时器干掉,自己在重新设置一个新的定时器,等待300ms
如果在这个时间内,再次点击,我们再把它干掉,再重新设置一个
timer是个数字,记录当前定时器的编号 clearTimeout(timer):按照编号清除定时器「但是timer的值还是之前的数字,
所以我们一般都会手动把timer设置为null,这样做的好处:如果timer=null我们就知道没有定时器,如果不为null则认为定时器还没有清除」
let timer = setTimeout(() => {
}, 300);
公共封装:
const clearTimer = function clearTimer(timer) { if (timer) clearTimeout(timer); return null; } /* \* debounce:函数防抖 \* @params \* func:自己最终要执行的任务 \* wait:多久操作一次算是频繁触发「默认值:500ms」 \* immediate:控制触发的边界 「默认值:false结束边界 true开始边界」 \* @return \* operate处理函数,处理函数会在频繁触发的时候,频繁执行;函数内部,控制我们想要操作的func只执行一次; */ // 具备公共性的防抖函数处理:在用户频繁操作「频繁的规则自己设定」的场景中,我们只识别一次操作即可「识别第一次、识别最后一次」 const debounce = function debounce(func, wait, immediate) { if (typeof func !== 'function') throw new TypeError('func is not a function'); //debounce(func,true); if (typeof wait === 'boolean') immediate = wait; if (typeof wait !== 'number') wait = 300; //debounce(func); //debounce(func,wait); if (typeof immediate !== 'boolean') immediate = false; let timer = null; return function operate(...params) { //判断是否立即执行 let now = !timer && immediate, result; timer = clearTimer(timer); timer = setTimeout(() => { // 因为方法执行的时候前面没有点,所以this指向的是window;为了让方法执行的时候this指向submit,需要使用call改变函数的this指向;让方法执行的时候,THIS是实参和没有debounce之前是一样的 // 最后执行「结束边界」 if (!immediate) func.call(this, ...params); // 清除最后一次设定的定时器;定时器代码中函数执行完把设置的定时器删除 timer = clearTimer(timer); }, wait); // 立即执行「开始边界」 if (now) result = func.call(this, ...params); return result; } } const handle = function handle() { console.log(`数据请求发送...`); setTimeout(() => { console.log(`数据请求成功...`); }, 3000); }; let submit = document.querySelector('#submit'); submit.onclick = debounce(handle, 300); /* submit.onclick = function operate(){ // 疯狂点击的按钮的时候,浏览器会疯狂的触发operate函数执行;我们只需要在operate中,控制handle只执行一次即可!! } */ ---------------------------------------------------- const submit = document.getElementById('submit'); submit.onclick = utils.debounce(function (ev) { console.log('click start', ev, this); query(val => { console.log('click end', val); }); }, 1000, true);只要“||”前面为false,不管“||”后面是true还是false,都返回“||”后面的值。只要“||”前面为true,不管“||”后面是true还是false,都返回“||”前面的值。
只要“&&”前面是false,无论“&&”后面是true还是false,结果都将返“&&”前面的值;只要“&&”前面是true,无论“&&”后面是true还是false,结果都将返“&&”后面的值;
节流
节流 throttle:在频繁操作的时候,我们能降低触发的频率 备注:频繁的频率自己来设定
运用场景:
- scroll 事件,每隔一秒计算一次位置信息等
- 浏览器播放事件,每个一秒计算一次进度信息等
- input 框实时搜索并发送请求展示下拉列表,没隔一秒发送一次请求 (也可做防抖)
const clearTimer = function clearTimer(timer) { if (timer) clearTimeout(timer); return null; }; // 具备公共性的节流函数处理:在用户频繁操作的场景中,我们降低触发的频率 const throttle = function throttle(func, wait) { if (typeof func !== 'function') throw new TypeError('func is not a function~'); if (typeof wait !== 'number') wait = 300; let timer = null, //记录触发的时间 previous = 0; return function operate(...params) { let now = +new Date(), remaining = wait - (now - previous); if (remaining <= 0) { // 两次触发的间隔时间超过wait,我们让函数立即执行即可「例如:第一次...」 // 记录当前触发的时间,作为下一次比较的上一次时间 timer = clearTimer(timer); previous = +new Date(); return func.call(this, ...params); } else if (!timer) { // 如果没有设置过定时器,而且两次触发的间隔时间也不足wait,此时我们设置一个去等待执行即可 timer = setTimeout(() => { timer = clearTimer(timer); func.call(this, ...params); }, remaining); } }; }; const handle = function handle() { console.log('OK'); }; // window.onscroll = handle; //默认的触发频率:浏览器最快的反应时间 「例如谷歌浏览器是5~7ms」 window.onscroll = throttle(handle); /* window.onscroll = function operate() { //operate函数会在滚动中,间隔5~7ms触发一次;而在这个函数中,我们基于一些逻辑运算,控制handle间隔300ms触发一次!! } */
面试经验:
// 谈谈你对闭包的理解「开放性问题」
// 把握住几个维度:概念「基础理论知识」、结合实战、高阶应用「含源码阅读」、插件/组件/类库封装
// 禁止背书式回答「技巧:设定一个场景,加一些铺垫词汇」
// 禁止事无巨细「技巧:只需要回答出核心关键,或者抛出一个“敏感”词汇,剩下的引导面试官去继续问自己」
// 孔子:温良恭俭让
// --------
// 作业:“剧本精神” 把这个问题的话术进行整理,大概整理3min左右的,写下来,后期面试之前背一下...