闭包
- 闭包:是指有权访问另一个函数作用域中的变量的函数。创建闭包的常见方式,就是在一个函数内部创建另一个函数。
- 示例
function createFunction() {
var result = new Array()
for (var i = 0; i < 10; i++) {
result[i] = function() {
return i;
}
}
return result;
} // 每个函数返回都是 10
//添加闭包
function createFunction() {
var result = new Array()
for (var i = 0; i < 10; i++) {
result[i] = function(num) {
return function() {
return num
}
}(i)
}
return result
}
this对象
- 在全局函数中,this等于window(非严格模式), 或者undefined(严格模式)
- 当函数作为某个对象的方法调用时,this等于那个对象,不过,
匿名函数
的执行环境具有全局性,因此其this对象通过指向window。
- 作为构造函数调用,this 指代new 的实例对象
- 通过apply() 或 call()改变函数执行环境的情况下,this就会指向其他对象。
- Node模块或ES6模块中,this返回的是当前模块
- 示例
var name = "The Window"
var object = {
name: "My Object",
getNameFunc: function() {
return function() {
return this.name
}
}
}
console.log(object.getNameFunc()())
// "The Window"(非严格模式) undefined(严格模式)
/*匿名函数的执行环境具有全局性,因此其this通过指向window*/
var name = "The Winodw"
var object = {
name: "My Object",
getName: function() {
return this.name;
}
}
object.getName() // "My Object"
(object.getName)() //"My Object"
(object.getName = object.getName)() // "The Window",在非严格格式下
闭包的作用
使用闭包可以在JavaScript中
模仿块级作用域
,要点如下:
- 创建并立即调用一个函数,这样既可以执行其中的代码,又不会在内存中留下对该函数的引用。
- 结果就是函数内部的所有变量都会被立即销毁——除非将某些变量赋值给了包含作用域(即外部作用域)中的变量。
闭包还可以用于在对象中
创建私有变量
,相关概念和要点如下:
- 即使JavaScript中没有正式的私有对象属性的概念,但可以使用闭包来实现公有方法,而通过公有方法可以访问在包含作用域中定义的变量。
- 有权访问私有变量的公有方法叫做特权方法。
- 可以使用构造函数模式、原型模式来实现自定义类型的特权方法,也可以使用模块模式、增强的模块模式来实现单例的特权方法。
模仿块级作用域
- javascript没有块级作用域的概念,这意味着在块级语句中定义的变量,实际上是在包含函数中而非语句中创建的。匿名函数可以用来模仿块级作用域并避免这个问题。用作块级作用域(通常称为私有作用域)的匿名函数的语法如下:
(function(){
// 这里是块级作用域
})()
- 以一个例子说明
例1:
function outputNumber() {
for (var i = 0; i < 10; i += 1) {}
console.log(i);
}
outputNumber();
// 此时会输出 10
例2:
function outputNumber() {
(function () {
for (var i = 0; i < 10; i += 1) {
console.log('i', i);
}
})();
console.log(i);
}
outputNumber();
添加一个匿名立即执行函数,此时就会报错 Uncaught ReferenceError: i is not defined
, 达到了块级作用域的效果
私有变量
- 严格来讲,javascript中没有私有成员的概念,所有对象属性都是公有的。不过,倒是有一个私有变量的概念。任何在函数中定义的变量,都可以认为是私有变量,因为不能在函数的外部访问这些变量。
- 如果在这个函数内部创建一个闭包,那么闭包通过t自己的作用域链也可以访问这些变量。而利用这一点,就可以创建用于说私有变量的公有方法。
我们把有权访问私有变量和私有函数的公有方法称为
特权方法
。有两种在对象上创建特权方法的方式。1. 在构造函数中定义特权方法
- 示例
function myObject() {
// 私有变量和私有函数
var privateVariable = 10;
function privateFunction() {
return false;
}
// 特权方法
this.publicMethod = function () {
privateVariable++;
return privateFunction();
};
}
在构造函数中定义特权方法的缺点:必须使用构造函数模式来达到目的,构造函数模式的缺点是针对每个实例都会创建同样一组新方法,而使用静态私有变量来实现特权方法就可以避免这个问题
2. 静态私有变量
- 通过在私有作用域中定义私有变量或函数,同样也可以创建特权方法。
- 示例
(function () {
// 私有变量和私有函数
var privateVariable = 10;
function privateFunction() {
return false;
}
// 构造函数
MyObject = function () {};
// 特权方法
MyObject.prototype.publicMethod = function () {
privateVariable++;
return privateFunction();
};
})();
这个模式与在构造函数中定义特权方法的主要区别,就在于私有变量和函数是由实例共享的。由于特权方法是在原型上定义的,因此所有实例都使用同一个函数。而这个特权方法,作为一个闭包,总是保存着对包含作用域的引用。
模块模式
- 模块模式则是为单例创建私有变量和特权方法。所谓单例,指的就是只有一个实例的对象。javascript是以对象字面量的方式来创建单例对象的。
- 示例
var singleton = {
name:value,
method: function(){
// do something
}
}
- 模块模式通过为单例添加私有变量和特权方法能够使其得到增强
var singleton = (function () {
// 私有变量和私有函数
var privateVariable = 10;
function privateFunction() {
return false;
}
// 特权方法和属性
return {
publicProperty: true,
publicMethod: function () {
privateVariable++;
return privateFunction();
},
};
})();
增加的模块模式
- 增强的模块模式适合那些单例必须是某种类型的实例,同时还必须添加某些属性/方法对其加以增强的情况。
- 示例
var application = function () {
var components = new Array();
components.push(new BaseComponent());
var app = new BaseComponent();
app.getComponentCount = function () {
return components.length;
};
app.registerComponent = function (component) {
if (typeof component === 'object') {
components.push(component);
}
};
return app;
};
// 限制application必须是BaseComponent的实例
过时的闭包
- 问题示例
function createIncrement(i) {
let value = 0;
function increment() {
value += i;
console.log(value);
const message = `Current value is ${value}`;
return function logValue() {
console.log(message);
};
}
return increment;
}
const inc = createIncrement(1);
const log = inc(); // 打印 1
inc(); // 打印 2
inc(); // 打印 3
// 无法正确工作
log(); // 打印 "Current value is 1"
修复过时闭包:
- 解决过时闭包的第一种方法是找到捕获最新变量的闭包。就是从最后一次调用 inc() 返回的闭包。
const inc = createIncrement(1);
inc(); // 打印 1
inc(); // 打印 2
const latestLog = inc(); // 打印 3
// 正常工作
latestLog(); // 打印 "Current value is 3"
2.第二种方法是让logValue()直接使用 value
function createIncrement(i) {
let value = 0;
function increment() {
value += i;
console.log(value);
return function logValue() {
const message = `Current value is ${value}`;
console.log(message);
};
}
return increment;
}
const inc = createIncrement(1);
const log = inc(); // 打印 1
inc(); // 打印 2
inc(); // 打印 3
// 正常工作
log(); // 打印 "Current value is 3"
React Hook
useEffect中过时闭包的问题及解决
- 示例
import React, { useEffect, useState } from 'react';
const HomePage = () => {
const [count, setCount] = useState(0);
const [count1, setCount1] = useState(0);
const [count2, setCount2] = useState(0);
let tm;
useEffect(() => {
setInterval(() => {
setCount(count + 1);
}, 1000);
}, []);
useEffect(() => {
setInterval(() => {
setCount1((prev) => {
return prev + 1;
});
}, 1000);
}, []);
useEffect(() => {
tm = setInterval(() => {
setCount2(count2 + 1);
}, 1000);
return () => {
clearInterval(tm);
tm = null;
};
}, [count2]);
return (
<div>
{/* count 一直显示为 1 */}
<p>计数:{count}</p>
{/* 以下两种方式正常计数 */}
<p>计数-callback:{count1}</p>
<p>计数-依赖项:{count2}</p>
</div>
);
};
防抖与节流
- 节流:每隔一段时间触发一次事件
// 方式1:
const throlttle3 = (fn, delay) => {
let flag = true;
return function () {
if (flag) {
flag = false;
setTimeout(() => {
fn.apply(this, arguments);
flag = true;
}, delay);
}
};
};
// 方式2 : 时间戳
const throlttle = (fn, delay) => {
let prev = Date.now();
return function () {
const now = Date.now();
if (now - prev >= delay) {
fn.apply(this, arguments);
prev = Date.now();
}
};
};
// 方式3: 定时器
const throlttle2 = (fn, delay) => {
let tm = null;
return function () {
if (!tm) {
tm = setTimeout(() => {
fn.apply(this, arguments);
}, delay);
}
};
};
// 第一次立即执行
const throlttle4 = (fn, delay) => {
let tm = null;
let lastTm = Date.now();
return function () {
let curTm = Date.now();
var rest = delay - (curTm - lastTm);
clearTimeout(tm);
if (rest <= 0) {
fn.apply(this, arguments);
lastTm = Date.now();
} else {
tm = setTimeout(() => {
fn.apply(this, arguments);
}, rest);
}
};
};
- 防抖:每次触发事件时设置一个延迟调用方法,并且取消之前的延时调用方法。
const debounce = (fn, delay) => {
``;
let tm = null;
return function () {
if (tm) clearTimeout(tm);
tm = setTimeout(() => {
fn.apply(this, arguments);
}, delay);
};
};
柯里化
- 是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数而且返回结果的新函数的技术。
- 示例
// 实现一个add方法,使计算结果能够满足如下预期:
add(1)(2)(3) = 6;
add(1, 2, 3)(4) = 10;
add(1)(2)(3)(4)(5) = 15;
function add() {
// 第一次执行时,定义一个数组专门用来存储所有的参数
var _args = Array.prototype.slice.call(arguments);
// 在内部声明一个函数,利用闭包的特性保存_args并收集所有的参数值
var _adder = function() {
_args.push(...arguments);
return _adder;
};
// 利用toString隐式转换的特性,当最后执行时隐式转换,并计算最终的值返回
_adder.toString = function () {
return _args.reduce(function (a, b) {
return a + b;
});
}
return _adder;
}
add(1)(2)(3) // 6
add(1, 2, 3)(4) // 10
add(1)(2)(3)(4)(5) // 15
add(2, 6)(1) // 9
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。