js button onclick 传参_JS基础进阶 闭包作用域和JS高阶编程技巧

关注“ 前端学苑 ” ,坚持每天进步一点点

e0f013a32ca534ecc040c7e2a3ef071b.png

「~JS基础进阶 ~」

* 学习进阶方式 ?

基础知识要夯实;

原理源码要深入;

深度广度要扩展;

简短的概括:

1、闭包作用域

2、闭包应用:循环事件绑定的解决方法

3、全面分析let和var的区别

4、JS高阶编程技巧 (闭包进阶应用)

1)应用1:循环事件绑定或者循环操作中对于闭包的应用

2)应用2:基于“闭包”实现早期的“模块化”思想

3)应用3:惰性函数(思想)

4)应用4:柯里化函数 & 重写reduce

5)应用5:compose组合函数

6)应用6:jQuery(JQ)中关于闭包的使用

7)应用7:函数的防抖和节流


闭包作用域


* 核心答案 | 基础知识要夯实

? 例如一:闭包作用域 - 面试题面试常问 )

let x = 5;
function fn(x) {
    return function (y) {
        console.log(y + (++x)); // 结果:14, 18, 18
    }
}
let f = fn(6);
f(7);
fn(8)(9);
f(10);
console.log(x);  // 结果:5

画图分析:( 有图有真相 )

80158582689af614b1f6ed172a2f79a7.png

例如二:闭包作用域 - 面试题面试常问 )

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

画图分析:( 有图有真相 )

db025483d5ea5c969e3110cf2e355343.png


闭包应用:循环事件绑定的解决方法


* 核心答案 | 基础知识要夯实

例子一: 递归导致内存溢出 (函数执行中再次调用自己执行)

// 下面案例是“死递归”  Uncaught RangeError: Maximum call stack size exceeded “内存溢出”
function fn(x) {
    // console.log(x);
    fn(x + 1);
}
fn(1); 

例子二: 循环事件绑定

* 需求分析:有三个按钮,依次单击按钮出现1,2,3。(深入分析)

HTML结构代码:


我是第1个按钮
我是第2个按钮
我是第3个按钮

js代码:

// 实现不了:我们要清楚原因
var buttons = document.querySelectorAll('button'); //=>NodeList“类数组”集合
for (var i = 0; i     buttons[i].onclick = function () {
        console.log(`当前点击按钮的索引:${i}`); //结果:3,3,3
    };

画图分析:( 有图有真相 )

c95a707616a61a98aa549e900b795097.png

实现不了主要原因:

每次点击触发函数执行,i获取的都是全局的,也就是循环后的结果3。

最终的解决方案:

* 方案一:基于“闭包”的机制完成 

核心:每一轮循环都产生一个闭包,“存储对应的索引”;点击事件触发,执行对应的函数,让其上级上下文是闭包即可。

? 例子一:

var buttons = document.querySelectorAll('button');
for (var i = 0; i     // 每一轮循环都会形成一个闭包,存储私有变量i的值(当前循环传递的i的值)//   + 自执行函数执行,产生一个上下文EC(A)  私有形参变量i=0/1/2//   + EC(A)上下文中创建一个小函数,并且让全局buttons中的某一项占用创建的函数
    (function (i) {
        buttons[i].onclick = function () {
            console.log(`当前点击按钮的索引:${i}`);
        };
    })(i);

画图分析:( 有图有真相 )

9fa3829b55447a1d8d3238dc98303db1.png

例子二:

var buttons = document.querySelectorAll('button');
for (var i = 0; i     buttons[i].onclick = (function (i) {
        return function () {
            console.log(`当前点击按钮的索引:${i}`);
        };
    })(i);

* 解释如下:( 重要 )

var obj = {  // 把自执行函数执行的返回值 (小函数)赋值给fn
    fn: (function () {  // 闭包
      console.log('大函数');
      return function () {
        console.log('小函数');
      }
  })()
};
obj.fn(); // 执行的是返回的小函数

例子三:

// 基于LET这种玩法也是“闭包”方案
let buttons = document.querySelectorAll('button');
for (let i = 0; i     buttons[i].onclick = function () {
        console.log(`当前点击按钮的索引:${i}`);
    };

总结:闭包的方式不仅 浪费了堆内存,也浪费了栈内存。

* 方案二:自定义属性 「性能强于闭包」

var buttons = document.querySelectorAll('button');
for (var i = 0; i     // 每一轮循环都给当前按钮(对象)设置一个自定义属性:存储它的索引
    buttons[i].myIndex = i;

    buttons[i].onclick = function () {
        // this -> 当前点击的按钮
        console.log(`当前点击按钮的索引:${this.myIndex}`);
    };

总结:自定义属性的方式虽然 栈内存没有浪费,但堆内存还有。

* 方案三:事件委托 「比之前的性能提高40%-60%」

核心:不论点击BODY中的谁,都会触发BODY的点击事件;ev.target的事件源:具体点击的是谁。

document.body.onclick = function (ev) {
    var target = ev.target,
        targetTag = target.tagName;

    // 点击的是BUTTON按钮
    if (targetTag === "BUTTON") {
        var index = target.getAttribute('index');
        console.log(`当前点击按钮的索引:${index}`);
    }
};

总结:事件委托的方式堆内存没有,栈内存也没有,只有一个堆函数。

b7abbd8ab0f3d59bf8dacbebc1727504.png

全面分析let和var的区别


* 核心答案 | 基础知识要夯实

声明方式

变量提升

作用域

初始值

重复定义

const

块级

需要

不允许

let

块级

不需要

不允许

var

函数级

不需要

允许

* 1、let VS const

1)let声明一个变量,变量存储可以改值;

2)const声明的变量,一但赋值,则不能再和其他的值关联(不允许指针重新指向);

let n = 12;
n = 13;
console.log(n); //13
const obj = {
    name: "前端学苑"
};
obj.name = "小贾";
console.log(obj); // {name: "小贾"}

* 2、let VS var

1)var存在变量提升,而let不存在

2)“全局上下文中”,基于var声明的变量,也相当于给GO(全局对象 window)新增一个属性,并且任何一个发生值的改变,另外一个也会跟着变化(映射机制);但是基于let声明的变量,就是全局变量,和GO没有任何的关系;

let n = 12;
console.log(n); //12
console.log(window.n); //undefined
// VO(G): var n=12;   <=>  GO(window): window.n=12;
var n = 12;
console.log(n); //12
console.log(window.n); //12
window.n = 13;
console.log(n); //13
// 1、没有基于任何关键词声明的,则相当于给window设置一个属性
n = 13; //window.n=13;  
// 2、首先看是否为全局变量,如果不是,则再看是否为window的一个属性...
console.log(n); //13  
// 3、如果两者都不是,则变量未被定义 Uncaught ReferenceError
console.log(m);  // m is not defined
function fn() {
    // 私有的上下文
    m = 13; //window.m=13 按照作用域链查找机制,当前变量找到全局都没有,则相当于给window设置一个属性
    console.log(m); //13
    console.log(n); //如果是获取的操作,则直接报错 Uncaught ReferenceError: n is not defined
}
fn();
console.log(m); //13 

3) 在相同的上下文中,let不允许重复声明(不论你之前基于何种方式声明,只要声明过,则都不能基于let重复声明了);而var很松散,重复声明也无所谓,反正浏览器也只按照声明一次处理;

4) 暂时性死区「浏览器暂存的BUG」;

console.log(n); //Uncaught ReferenceError: n is not defined
console.log(typeof n); //undefined 基于typeof检测被声明的变量,结果是undefined

5) let/const/function会产生块级私有上下文,而var是不会的。

* 3、上下文 & 作用域:

1)全局上下文;

2)函数执行形成的“私有上下文”;

3)块级作用域(块级私有上下文) 除了 对象/函数... 的大括号之外(例如:判断体、循环体、代码块...)都可能会产生块级上下文。

// n是全局上下文的:代码块不会对他有任何的限制// m是代码块所代表的块级上下文中私有的// debugger; //开启断点调试  =>BUG调试
{
    var n = 12;
    console.log(n); //12

    let m = 13;
    console.log(m); //13
}
console.log(n); //12
console.log(m); //Uncaught ReferenceError: m is not defined
// i是全局的
for (var i = 0; i 5; i++) {
    console.log(i); //0~4
}
console.log(i); //5 


// i不是全局的,上下文的私有的
for (let i = 0; i 5; i++) {
    console.log(i); //0~4
}
console.log(i); //Uncaught ReferenceError: i is not defined

* 推荐写法:

// i及循环中用到的i都是在全局下声明的全局变量(循环不会产生块级上下文)
let i = 0;
for (; i 5; i++) {
    console.log(i); //0~4
}
console.log(i); //5

b7abbd8ab0f3d59bf8dacbebc1727504.png


JS中最基本的this情况分析


* 核心答案 | 基础知识要夯实

this函数的执行主体 (注:对象后面分析)

1)函数执行主体:谁把函数执行的;

2)函数执行上下文:在哪执行的;

// 全局上下文中的this是window;// 块级上下文中没有自己的this,所用到的this都是所处上级上下文中的this;
console.log(this); //window
{
    let n = 12;
    console.log(this); //window  上级上下文是全局上下文
}

规律:

1)事件绑定:给当前元素的某个事件行为绑定方法,当事件触发、方法执行,方法中的this是当前元素本身;

2)普通函数执行

1. 函数执行前面是否有“点”,没有“点”,this就是window(或者JS严格模式下是undefined);

2. 有“点”,“点”前面是谁this就是谁;

3.匿名函数(自执行函数/回调函数)如果没有经过特殊的处理,则this一般都是window/undefined,但是如果经过一些特殊处理,一切都以处理后的结果为主;

1、事件绑定

document.body.onclick = function () {
    console.log(this); //body
};
document.body.addEventListener('click', function () {
    console.log(this); //body
});
// IE6~8 DOM2事件绑定
document.body.attachEvent('onclick', function () {
    console.log(this); //window
}); 

2、普通函数执行

function sum () {
  console.log(this); // 结果:undefined
}

说明:创建函数,没有执行。this不知道是谁。

1)有“点”,“点”前面是谁this就是谁

// 开启JS严格模式  => 扩展:严格模式和非严格模式的区别// "use strict";
function fn() {
    console.log(this);
}
let obj = {
    name: '前端学苑',
    fn: fn
};
fn(); //this->window/undefined
obj.fn(); //this->obj 

2)匿名函数

(function () {
    console.log(this); //this->window/undefined
})();

说明:开启严格模式是undefined

function fn(callback) {
    callback(); //this->window/undefined
}
let obj = {
    // sum:function(){}
    sum() {
        console.log(this); // window
    }
};
// obj.sum(); //this->obj// 回调函数:把一个函数作为实参值,传递给另外一个函数// => fn(function () {});
fn(obj.sum); 
setTimeout(function () {
    console.log(this); //window或者undefined
}, 1000);
let obj = {
    name: 'xxx'
};
let arr = [10, 20];
arr.forEach(function (item, index) {
    console.log(this);  // this -> obj
}, obj);

说明:obj 因为触发回调函数执行的时候,forEach内部会把回调函数中的this改变为传递的第二个参数值obj “特殊处理”。

let obj = {
    sum() {
        console.log(this);
    }
};
obj.sum(); //this->obj
(obj.sum)(); //this->obj
(10, obj.sum)(); //this->window 

说明:括号表达式,小括号中包含“多项”(如果只有一项,和不加括号没啥本质区别),其结果是只取最后一项;但是这样处理后,this会发生改变,变为window/undefined。

b7abbd8ab0f3d59bf8dacbebc1727504.png


JS高阶编程技巧


* 核心答案 | 基础知识要夯实

JS高阶编程技巧「本质:基于“闭包”的机制完成的」

* 应用1:循环事件绑定或者循环操作中对于闭包的应用

* 面试题 面试常问 )

for (var i = 0; i 3; i++) {
    setTimeout(() => { // 设置定时器的时候,这个函数是创建不是执行
        console.log(i); // 结果:3, 3, 3
    }, (i + 1) * 1000);

分析:

setTimeout([function],[interval]):设置一个定时器,等待[interval]这么长的时间后,触发[function]执行。

------i是全局下的变量

i=0 第一轮循环

   setTimeout(() => {  //设置定时器的时候,这个函数是创建不是执行

       console.log(i);

   }, 1000);

i=1 第二轮循环

   setTimeout(() => {

       console.log(i);

   }, 2000);

i=2 第三轮循环

   setTimeout(() => {

       console.log(i);

   }, 3000);

i=3 循环结束  

------1000ms时间到了

执行 () => {console.log(i);} 这个函数

1)在形成的私有上下文中遇到变量i,发现并不是自己私有的,找上级上下文(全局)下的i;

2)结果都是循环结束后的3;

如何实现结果是:0,1,2 ?

例子一:

for (var i = 0; i 3; i++) {
    // 每一轮循环,自执行函数执行,都会产生一个私有上下文;并且是把当前这一轮循环,全局变量i的值作为实参,传递给私有上下文中的形参i//   + EC(AN1)  形参赋值:i=0//   + EC(AN2)  形参赋值:i=1//   + EC(AN3)  形参赋值:i=2// -> 每一个形成的私有上下文中,都会创建一个“箭头函数堆”,并且把其赋值给 window.setTimeout ,// 这样等价于,当前上下文中的某些内容,被上下文以外的东西给占用了,形成的上下文不会释放(私有变量i的值也不会被释放) 「闭包」
    (function (i) {
        setTimeout(() => {
            console.log(i); 结果:0,1,2
        }, (i + 1) * 1000);
    })(i);
}

例子二:

// let xxx=proxy(0) -> proxy执行会产生闭包,闭包中私有的形参变量存储传递的实参信息
const proxy = i => {
    return () => {
        console.log(i);
    };
};
for (var i = 0; i 3; i++) {
    setTimeout(proxy(i), (i + 1) * 1000);
    // 到达时间后,执行的是proxy返回的小函数
}

例子三:

for (let i = 0; i 3; i++) {
    setTimeout(() => {
        console.log(i);
    }, (i + 1) * 1000);

基于let的循环(let存在“块级作用域”的处理机制)

1)首先浏览器会创建一个父级私有的上下文,控制循环;

2)每一轮循环还会产生一个私有的块级上下文,都有自己的私有变量i,存储当前这一轮循环i的值;

3)EC(BLOCK1)  私有变量i=0;

4)EC(BLOCK2)  私有变量i=1;

5)EC(BLOCK3)  私有变量i=2;

-> 每一个私有块级上下文中,也是创建一个箭头函数,并且被window.setTimeout占用了,也一样不会释放这个块级上下文「闭包」。

和我们自己写代码形成“闭包”的区别:它是浏览器底层实现的,从性能上比我们自己写的要快那么一些。

* 性能对比

for (let i = 0; i 3; i++) {
    setTimeout(() => {
        // ... 处理很多事情,但是函数中不需要使用i的值
    }, (i + 1) * 1000);

每一轮循环都会产生一个私有的块级上下文,如果上下文中没有什么东西被外部占用,则本轮循环结束,私有块级上下文也会被释放掉;但是一旦有东西被占用,则会产生闭包,性能上会有所消耗。

let i = 0;
for (; i 3; i++) {
    setTimeout(() => {
        // ...
    }, (i + 1) * 1000);

这种写法,在循环的时候就不会产生块级上下文了,性能上比之前要还好一些。

* 应用2:基于“闭包”实现早期的“模块化”思想

1)单例设计模式(模块化概念)

2)AMD -> require.js

3)CMD -> sea.js

4)CommonJS -> Node本身就是基于这种规范实现的

5)ES6Module

解决变量冲突:闭包机制 -> 保护

(function () {
    let name = '张三';
    let age = 22;
    let girlfriend = false;
    const skill = function () {};

    // 把私有的信息暴露到全局上,这样在其他的上下文中,就可以调用这些信息了;// 不能暴露太多,暴露多了,也会导致冲突;
    window.skill = skill;
})();

(function () {
    let name = '李四';
    let age = 81;
    let girlfriend = false;

    skill();
})(); 

解决变量冲突:对象 -> 把描述当前事物特征的内容,全部汇总到一个对象中(存储到同一个堆内存中)

let person1 = {
    name: '张三',
    age: 22,
    girlfriend: false,
    skill: function () {}
};

let person2 = {
    name: '李四',
    age: 81,
    girlfriend: false,
}; 

说明:

1)对象在这里起到一个“分组”的作用。

2)新称呼:person1/person2 被称为命名空间;

此处相当于,把描述同一个事物的属性,存放到相同的命名空间下,以此来进行分组,减少全局变量的污染;

每一个对象都是Object这个类的一个实例,person1和person2是两个完全不同的实例,所以我们也可以把这种方式称之为“单例设计模式” -> 目的也是解决全局变量冲突和污染的;

扩展:

单例设计模式就是“破对象”;

new Xxx —> 构造函数模式;

高级单例设计模式:JS中最早期的模块化开发思想,“模块之间的独立性以及互通性”。

1)把一个复杂或者大型的产品,按照功能特点拆分成一个个的模块;

2)每个模块都是独立的,相互之间的信息互不干扰(有助于团队协作开发);

3)但是对于一些需要供其他模块用到的公共方法,我们是可以实现相互调用的;

? 模块化开发例子:

// 前端组长 -> 公共方法封装
let utils = (function () {
    let isWindow = true,
        num = 0;
    const queryElement = function queryElement(selector) {};
    const formatTime = function formatTime(time) {};

    // 把需要供外界调用的方法,存储到一个命名空间(对象)中
    return {
        // queryElement:queryElement
        queryElement,
        formatTime
    };
})();

// 程序猿A -> 搜索
let searchModal = (function () {
    let num = 10;
    const checkValue = function checkValue() {
        utils.formatTime();
    };
    const submit = function submit() {};

    return {
        checkValue
    };
})();

// 程序媛B -> 换肤
let skinModal = (function () {
    let num = 0;
    const submit = function submit() {
        searchModal.checkValue();
    };

    return {};
})();

* 应用3:惰性函数(思想)

懒:能够干一次的绝对不会干第二次。

需求:获取元素的样式?

思路:

1)获取元素的样式,使用window.getComputedStyle(element) 属性名获取,但不兼容IE678 * 若不兼容;

2)则使用element.currentStyle(属性名);

说明:属性名 in 对象:检测当前这个属性是否属于这个对象。

代码如下:

function get_css(element, attr) {
    if ('getComputedStyle' in window) {
        return window.getComputedStyle(element)[attr];
    }
    return element.currentStyle[attr];


var w = get_css(document.body, 'width');
console.log(w);

var h = get_css(document.body, 'height');
console.log(h);

性能上的问题:

在某个浏览器中渲染页面(渲染代码)

1)第一次执行 get_css 需要验证浏览器的兼容性;

2)后期每一次执行 get_css ,浏览器的兼容性检测都会执行一遍 “这个操作是没有必要的”;

* 改造,基于“惰性函数”提高上述的性能

function get_css(element, attr) {
    // 第一次执行get_css,根据浏览器的兼容情况,对外部的get_css函数进行重构
    if ('getComputedStyle' in window) {
        get_css = function (element, attr) {
            return window.getComputedStyle(element)[attr];
        };
    } else {
        get_css = function (element, attr) {
            return element.currentStyle[attr];
        };
    }
    // 第一次执行也是需要获取到结果的,所以我们把重构的函数执行一次
    return get_css(element, attr);
}

var w = get_css(document.body, 'width');
console.log(w);

// 后续再次执行get_css,执行是的是第一次重构后的小方法,无需再次校验兼容性
var h = get_css(document.body, 'height');
console.log(h);

* 应用4:柯里化函数 & 重写reduce

柯里化函数

预处理的思想:预先存储,后续拿来直接使用。

1)执行函数,形成一个闭包,把一些信息(私有变量和值)存储起来「保存作用」;

2)以后其下级上下文中如果需要用到这些值,直接基于作用域链查找机制,拿来直接用即可;

? 柯里化函数例子 (es5) :

function fn() {
   // 存储执行fn传递的实参信息
   let outerArgs = Array.from(arguments);
   return function anonymous() {
       // 存储执行小函数传递的实参信息
       let innerArgs = Array.from(arguments);
       // 存储两次执行函数传递的实参信息
       let params = outerArgs.concat(innerArgs);
       return params.reduce(function (result, item) {
           return result + item;
       });
   };
}

let res = fn(1, 2)(3);
console.log(res); //=>6  1+2+3

柯里化函数例子 (es6) :

const fn = (...outerArgs) => {
    return (...innerArgs) => {
        return outerArgs.concat(innerArgs).reduce((result, item) => {
            return result + item;
        });
    };
}; 
let res = fn(1, 2)(3);
console.log(res); //=>6  1+2+3

柯里化函数例子 (es6简写方式) :

const fn = (...outerArgs) => (...innerArgs) => outerArgs.concat(innerArgs).reduce((result, item) => result + item);
let res = fn(1, 2)(3);
console.log(res); //=>6  1+2+3

柯里化函数例子

function fn(x) {
    return function (y) {
        return function (z) {
            return x + y + z;
        }
    }
}
let res = fn(10)(20)(30);
console.log(res) // 60

* 数组求和方法:

1、eval方法处理
eval([10, 20, 30].join('+')) // 60

2、循环遍历数组中的每一项,以些实现求和
let arr = [10, 20, 30],
      total = 0;
arr.forEach(function(item){
    total += item;
})
console.log(total)  // 60

数组 reduce

数组中的reduce:依次遍历数组中的每一项,可以把上一轮遍历得到的结果,传递给下一轮,以此实现结果的累计。

1)arr.reduce([function]):会把数组第一项做为初始结果,从数组第二项开始遍历;

2)arr.reduce([function],[value]):第二个传递的参数作为初始结果,从数据第一项开始遍历;

reduce例子一:

let arr = [10, 20, 30, 40];
let result = arr.reduce(function (result, item, index) {
   // 第一轮遍历: result->10「数组中的第一项值」  item->20  index->1// 第二轮遍历: result->30「上一轮遍历,函数返回的结果」 item->30 index->2// 第三轮遍历: result->60 item->40 index->3
   console.log(result, item, index);
   return result + item;
}); 

reduce例子二:

let arr = [10, 20, 30, 40];
let result = arr.reduce(function (result, item, index) {
    // 第一轮遍历: result->0  item->10 index->0// 第二轮遍历: result->10 item->20 index->1// ....
    console.log(result, item, index);
    return result + item;
}, 0);
console.log(result); //100

* 重构reduce-面试题 面试常问 )

function reduce(arr, callback, initValue) {
    let result = initValue,
        i = 0;
    // 没有传递initValue初始值:把数组第一项作为初始值,遍历从数组第二项开始
    if (typeof result === "undefined") {
        result = arr[0];
        i = 1;
    }
    // 遍历数组中的每一项:每一次遍历都会把callback执行
    for (; i         result = callback(result, arr[i], i);
    }
    return result;
}

let arr = [10, 20, 30, 40];
let result = reduce(arr, function (result, item, index) {
    return result + item;
});
console.log(result); //结果;100 

* 应用5:compose组合函数

命令式编程:注重过程的管控;

函数式编程:注重最后的结果 (大部分需求,函数式更好);

在函数式编程当中有一个很重要的概念就是函数组合, 实际上就是把处理数据的函数像管道一样连接起来, 然后让数据穿过管道得到最终的结果。例如:

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(div2, mul3, add1, add1)
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;

// funcs:按照管道的顺序依次存储着要处理的函数
const compose = (...funcs) => {
    return x => {
        let len = funcs.length;
        if (len === 0) return x;
        if (len === 1) return funcs[0](x);
        return funcs.reduceRight((result, item) => {
            return item(result);
        }, x);
    };
}; 

const operate = compose(div2, mul3, add1, add1);
console.log(operate(0)); //3
console.log(operate(2)); //6
console.log(compose()(10)); //10
console.log(compose(div2)(10)); //5

* 简写方式 (需要深入)

const compose = (...funcs) => {
    let len = funcs.length;
    if (len === 0) return x => x;
    if (len === 1) return funcs[0];
    return funcs.reduce((a, b) => {
        return x => {
            return a(b(x));
        };
    });
};
// [div2, mul3, add1, add1]//第一轮遍历 a=div2 b=mul3 //   x=>a(b(x))  0x000//第二轮遍历 a=0x000 b=add1//   x=>a(b(x))  0x001//第三轮遍历 a=0x001 b=add1//   x=>a(b(x))  0x002//--->最后返回的就是 0x002,把其赋值给 operate// 0x002(0)  -> 0x001(add1(0)) // 0x001(1)  -> 0x000(add1(1))// 0x000(2)  -> div2(mul3(2)) =>3

* 应用6:jQuery(JQ)中关于闭包的使用

1、第一代码块
let params1 = typeof window !== "undefined" ? window : this;

利用JS的暂时性死区:基于typeof检测一个未被声明的变量,不会报错,结果是undefined。

1)浏览器环境下 & APP的webview环境下:默认就有window -> GO;

2)Node环境下:没有window,this->global || 当前模块;

2、第二代码块

let params2 = function (window, noGlobal) {
    // 浏览器环境下:window->window noGlobal->undefined// Node环境下:window->this noGlobal->true
    var jQuery = function (selector, context) {
        // ...
    };
    // ...// 为了防止我们暴露到全局的jQuery和$,和全局下现有的内容冲突,我们可以提供$等使用权限的转让
    var _jQuery = window.jQuery,
        _$ = window.$;
    jQuery.noConflict = function noConflict(deep) {
        if (window.$ === jQuery) {
            window.$ = _$;
        }
        if (deep && window.jQuery === jQuery) {
            window.jQuery = _jQuery;
        }
        return jQuery;
    };

    // 浏览器环境下,基于window.xxx=xxx的方式,把jQuery暴露到全局//   jQuery() 或者 $() -> 都是把内部私有的jQuery执行
    if (typeof noGlobal === "undefined") {
        window.jQuery = window.$ = jQuery;
    }

    return jQuery;
};

3、第三代码块 ( 区分环境 )

(function (global, factory) {
    // 浏览器环境下:global->window   Node环境下:global->当前module// factory:回调函数
    "use strict";
    if (typeof module === "object" && typeof module.exports === "object") {
        // 支持CommonJS模块规范的环境:Node环境// -> params2(this,true)
        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规范)// -> params2(window)
        factory(global);
    }
})(params1, params2);

* 不同类库之间对$使用权的冲突

var _jQuery = window.jQuery,
  _$ = window.$;
// 当前类库没有导入完之前,把之前全局的$和 jQuery存储起来
jQuery.noConflict = function (deep) {
// 发现有冲突执行这个方法,把$的使用权归还给之前使用它的人
 if (window.$ === jQuery) {
  window.$ = _$;
 }
  // 传递deep = true还可以把 jQuery名字的使用仅也转移出去
 if (deep && window.jQuery === jQuery) {
  // 返回自己的:在外面基于一个别名接受,以后别名代表当前自己的
  window.jQuery = _jQuery;
 }
  // 让现在全局下的$和jQuery都以最新自己导入的为主
 return jQuery;
};

* 应用7:函数的防抖和节流

1、函数的防抖(防止老年帕金森)

对于频繁触发某个操作,我们只识别一次(只触发执行一次函数)

function debounce(func, wait = 300, immediate = false) {
    let timer = null;
    return function anonymous(...params) {
        let now = immediate && !timer;

        // 每次点击都把之前设置的定时器清除
        clearTimeout(timer);

        // 重新设置一个新的定时器监听wait时间内是否触发第二次
        timer = setTimeout(() => {
            // 手动让其回归到初始状态
            timer = null;
            // wait这么久的等待中,没有触发第二次
            !immediate ? func.call(this, ...params) : null;
        }, wait);

        // 如果是立即执行
        now ? func.call(this, ...params) : null;
    };
}

参数

1)func[function]:最后要触发执行的函数;

2)wait[number]:“频繁”设定的界限;

3)immediate[boolean]:默认多次操作,我们识别的是最后一次,但是immediate=true,让其识别第一次;

主体思路:

在当前点击完成后,我们等wait这么长的时间,看是否还会触发第二次,如果没有触发第二次,属于非频繁操作,我们直接执行想要执行的函数func;如果触发了第二次,则以前的不算了,从当前这次再开始等待...

应用场景

1)keyup 事件;

2)调整窗口大小;

2、函数节流:

在一段频繁操作中,可以触发多次,但是触发的频率由自己指定。

function throttle(func, wait = 300) {
    let timer = null,
        previous = 0; // 记录上一次操作的时间
    return function anonymous(...params) {
        let now = new Date(),
            remaining = wait - (now - previous); //记录还差多久达到我们一次触发的频率
        if (remaining <= 0) {
            // 两次操作的间隔时间已经超过wait了
            window.clearTimeout(timer);
            timer = null;
            previous = now;
            func.call(this, ...params);
        } else if (!timer) {
            // 两次操作的间隔时间还不符合触发的频率
            timer = setTimeout(() => {
                timer = null;
                previous = new Date();
                func.call(this, ...params);
            }, remaining);
        }
    };
}

参数

func[function]:最后要触发执行的函数;

wait[number]:触发的频率;

应用场景

1)鼠标不断点击触发;

2)监听滚动事件;

b7abbd8ab0f3d59bf8dacbebc1727504.png

* 简述你对闭包的理解,以及其优缺点?(面试常问)

1、为什么产生闭包

浏览器想要执行代码,都要进栈执行的。在进栈过程中, 由于某些内容被上下文以外的一些事物所占用,则当前上下文不能被出栈释放,根据浏览器的垃圾回收机制,如果被占用不释放,就会保留下来,这种机制就是闭包。

2、闭包的作用

1)保护:保护私有上下文中的“私有变量”和外界互不影响。

2)保存:上下文不被释放,那么上下文中的“私有变量”和“值”都会被保存起来,可以供其下级上下文中使用。

3、闭包弊端

如果大量使用闭包,会导致栈内存太大,页面渲染变慢,性能受到影响,所以真实项目中需要“合理应用闭包”;某些代码会导致栈溢出或者内存泄漏,这些操作都是需要我们注意的;

4、实战中的应用

1)循环事件绑定方法:可以用事件索引,后来修改是let的方式,都是闭包的方式。闭包的方式不仅 浪费了堆内存,也浪费了栈内存。这种方式不太好,所以用的事件委托。

2)惰性函数、柯里化函数、compose组合函数、函数的防抖和节流,jQuery源码也都用了闭包的思路。

3)自己封装组件,模块之间的独立性以及互通性。

总结:所以需要深入了解闭包,对项目有帮助的。


* 扩展:

一、GC:浏览器的垃圾回收机制(内存释放机制)

1、栈内存释放

1)加载页面,形成一个全局的上下文,只有页面关闭后,全局上下文才会被释放。

2)函数执行会形成一个私有的上下文,进栈执行;当函数中代码执行完成,大部分情况下,形成的上下文都会被出栈释放掉,以此优化栈内存大小;

2、堆内存释放

方案一:(例如谷歌):查找引用

浏览器在空闲或者指定时间内,查看所有的堆内存,把没有被任何东西占用的堆内存释放掉;但是占用着的是不被释放的;

方案二:(例如IE):引用计数

创建了堆内存,被占用一次,则浏览器计数+1,取消占用则计数 -1。当记录的数字为零的时候,则内存释放掉;某些情况会导致记数混乱出现“内存泄漏”的现象。

二、事件委托

让利用事件冒泡的原理,让自己的所触发的事件,让他的父元素代替执行。

e.target 事件目标(触发事件流的元素)

事件委托优点:可以为将来元素绑定事件;减少事件注册。

b7abbd8ab0f3d59bf8dacbebc1727504.png

推荐热门技术文章:

JS基础篇
  • JS基础进阶- 堆栈内存和闭包作用域

  • 深入理解JS事件循环机制

  • 彻底弄懂 “ 防抖 和 节流 ”

  • Babel插件入门-AST(抽象语法树)

b7abbd8ab0f3d59bf8dacbebc1727504.png

觉得本文对你有帮助?请分享给更多人

关注「前端学苑」加星标,提升前端技能

f70538e88747c21d5d1e09e30a5a84f4.png

如果觉得这篇文章还不错,来个【分享、点赞、在看】三连吧,让更多的人也看到~

3284355095f27de760c4ea13f8c3af03.gif

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值