JS高阶(三)闭包作用域

464 阅读19分钟

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 

Snipaste_2021-07-27_11-59-57

练习题:

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);
Snipaste_2021-07-28_00-26-50

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);

Snipaste_2021-07-28_01-19-32


// 函数是个渣男
debugger;
console.log(foo);
if (1 === 1) {
console.log(foo);
function foo() {}
foo = 1;
console.log(foo);
}
console.log(foo); 

Snipaste_2021-07-28_01-42-48

微信图片_20211121121056

//注意函数执行是没有返回值的


/*
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)>
​    初始THISwindow / 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);

Snipaste_2021-07-29_16-36-17

微信图片_20211121152030


let a=0,
​    b=0;
function A(a){
​    A=function(b){
​        alert(a+b++);
​    };
​    alert(a++);
}
A(1);
A(2);

Snipaste_2021-07-29_17-13-23

练习题

Snipaste_2021-08-03_23-21-45

Snipaste_2021-08-03_23-34-07


Snipaste_2021-08-03_23-47-39


Snipaste_2021-08-03_23-56-53


Snipaste_2021-08-04_00-04-53


Snipaste_2021-08-05_23-48-39


Snipaste_2021-08-06_00-06-58


套娃:

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);

Snipaste_2021-08-07_23-58-53


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);

Snipaste_2021-08-16_01-34-22


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}`);
    };
}

4-1

// 解决办法一:闭包解决方案,利用闭包的“保存”机制
// 每一轮循环的时候,都创建一个闭包(不释放的上下文),闭包中存储自己的私有变量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));
});

requirejs.org

Snipaste_2021-09-27_23-33-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源码分析【环境处理】

微信图片_20210805001016

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;
}); */

Snipaste_2021-11-30_13-30-16


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 框实时搜索并发送请求展示下拉列表,没隔一秒发送一次请求 (也可做防抖)

Snipaste_2021-12-01_22-35-14

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左右的,写下来,后期面试之前背一下...