S03-08 JS-高级-函数增强
[TOC]
闭包@
闭包-概念
又爱又恨的闭包
闭包是 JavaScript 中一个非常容易让人迷惑的知识点:
有同学在深入 JS 高级的交流群中发了这么一张图片;
并且闭包也是群里面大家讨论最多的一个话题;
闭包确实是 JavaScript 中一个很难理解的知识点,接下来我们就对其一步步来进行剖析,看看它到底有什么神奇之处。
JS的函数式编程
在前面我们说过,JavaScript 是支持函数式编程的
在 JavaScript 中,函数是非常重要的,并且是一等公民:
那么就意味着函数的使用是非常灵活的;
函数可以作为另外一个函数的参数,也可以作为另外一个函数的返回值来使用;
所以 JavaScript 存在很多的高阶函数:
自己编写高阶函数
使用内置的高阶函数
目前在 vue3 和 react 开发中,也都在趋向于函数式编程:
vue3 composition api: setup 函数 -> 代码(函数 hook,定义函数);
react:class -> function -> hooks
闭包的定义
这里先来看一下闭包的定义,分成两个:在计算机科学中和在 JavaScript 中。
维基百科定义:在计算机科学中对闭包的定义(维基百科):
闭包(Closure),又称词法闭包(Lexical Closure)或函数闭包(function closures);
是在支持头等函数的编程语言中,实现词法绑定的一种技术;
闭包在实现上是一个结构体,它存储了一个函数和一个关联的环境(相当于一个符号查找表);
闭包跟函数最大的区别在于,当捕捉闭包的时候,它的自由变量会在捕捉时被确定,这样即使脱离了捕捉时的上下文,它也能照常运行;
历史:闭包的概念出现于 60 年代,最早实现闭包的程序是 Scheme,那么我们就可以理解为什么 JavaScript 中有闭包:
- 因为 JavaScript 中有大量的设计是来源于 Scheme 的;
MDN定义:我们再来看一下MDN对 JavaScript 闭包的解释:
一个函数和对其周围状态(lexical environment,词法环境)的引用捆绑在一起(或者说函数被引用包围),这样的组合就是闭包(closure);
也就是说,闭包让你可以在一个内层函数中访问到其外层函数的作用域;
在 JavaScript 中,每当创建一个函数,闭包就会在函数创建的同时被创建出来;
DeepSeek:
闭包(Closure) 是指一个函数能够记住并访问其所在的词法作用域(Lexical Scope),即使该函数在其词法作用域之外执行。简单来说,闭包允许函数访问其定义时所在的作用域中的变量,即使该作用域已经销毁。闭包的本质是函数与其词法作用域的结合。
自己总结:那么我的理解和总结:
一个普通的函数function,如果它可以访问外层作用域的自由变量**,那么这个函数和周围环境就是一个**闭包****;
从广义的角度来说:JavaScript 中的函数都是闭包;
从狭义的角度来说:JavaScript 中一个函数,如果访问了外层作用域的变量,那么它是一个闭包;
闭包-形成过程
闭包的访问过程
如果我们编写了如下的代码,它一定是形成了闭包的:
闭包的执行过程
那么函数继续执行呢?
这个时候 makeAdder 函数执行完毕,正常情况下我们的 AO 对象会被释放;
但是因为在 0xb00 的函数中有作用域引用指向了这个 AO 对象,所以它不会被释放掉;
闭包-内存泄露
闭包的内存泄漏
那么我们为什么经常会说闭包是有内存泄露的呢?
在上面的案例中,如果后续我们不再使用 add10 函数了,那么该函数对象应该要被销毁掉,并且其引用着的父作用域 AO 也应该被销毁掉;
但是目前因为在全局作用域下 add10 变量对 0xb00 的函数对象有引用,而 0xb00 的作用域中 AO(0x200)有引用,所以最终会造成这些内存都是无法被释放的;
所以我们经常说的闭包会造成内存泄露,其实就是刚才的引用链中的所有对象都是无法释放的;
那么,怎么解决这个问题呢?
因为当手动将 add10 设置为 null时,就不再对函数对象 0xb00 有引用,那么对应的 AO 对象 0x200 也就不可达了;
在 GC 的下一次检测中,它们就会被销毁掉;
闭包的内存泄漏测试
AO不使用的属性优化
我们来研究一个问题:AO 对象不会被销毁时,是否里面的所有属性都不会被释放?
下面这段代码中 name 属于闭包的父作用域里面的变量;
我们知道形成闭包之后 count 一定不会被销毁掉,那么 name 是否会被销毁掉呢?
这里我打上了断点,我们可以在浏览器上看看结果;
函数增强
类数组对象
概述
类数组对象(Array-like Object):是 JavaScript 中一种特殊的对象类型,它具有类似数组的结构(数字索引和 length
属性),但不具备数组的原生方法(如 push
、pop
、forEach
等)。这类对象在 JavaScript 中非常常见,通常出现在与 DOM 操作、函数参数处理等场景中。
类数组对象的特征:
- 数字索引属性:可以通过
[0]
、[1]
等数字索引访问元素。 length
属性:表示元素的个数,与数组的length
行为一致。- 不具备数组方法:无法直接调用
push()
、slice()
等数组方法。
常见的类数组对象:
- argumets 对象:函数内部通过 arguments 访问传入的参数列表。
- DOM 集合:
HTMLCollection
:由 document.getElementsByTagName() 返回。NodeList
:由 document.querySelectorAll() 返回。
- 字符串:字符串本质上是类数组对象,每个字符对应一个数字索引。
转数组类型方法
转数组类型方法:由于类数组对象无法直接使用数组方法,通常需先将其转换为真正的数组。
方法1:
Array.from()
jsconst args = Array.from(arguments); args.push(3); // 现在可以使用数组方法
方法2:扩展运算符(
...
)jsconst nodeList = document.querySelectorAll('div'); const divArray = [...nodeList]; // 转为数组 divArray.forEach(div => console.log(div));
方法3:
Array.prototype.slice.call()
jsfunction sum() { const args = Array.prototype.slice.call(arguments); args.push(3); // 现在可用数组方法 }
判断类数组对象
判断类数组对象:
原理:检查对象是否满足以下条件:
- 是对象:
typeof obj === 'object'
且不为null
- 有 length 属性:
obj.length >= 0
- 有数字索引属性:如
obj[0]
存在
实现代码:
function isArrayLike(obj) {
if (obj == null || typeof obj !== 'object') return false;
const length = obj.length;
return typeof length === 'number' &&
length >= 0 &&
(length === 0 || (length > 0 && (length - 1) in obj));
}
剩余参数
剩余参数(Rest Parameters):是 ES6 引入的一种语法特性,允许函数接收不定数量的参数,并将这些参数自动转换为一个数组。它通过 ...
符号定义,使得处理可变参数更加简洁和灵活。
语法特性:
语法定义:使用
...参数名
作为函数的最后一个形参,收集剩余的所有参数:jsfunction sum(...numbers) { return numbers.reduce((total, num) => total + num, 0); } console.log(sum(1, 2, 3)); // 6
必须是最后一个参数:剩余参数只能位于参数列表的末尾,否则会报错:
js// 错误示例 function invalid(a, ...rest, b) {} // SyntaxError
与普通参数共存:可与其他参数结合使用,剩余参数收集“剩下的”参数:
jsfunction greet(greeting, ...names) { console.log(`${greeting}, ${names.join(', ')}!`); } greet("Hello", "Alice", "Bob"); // "Hello, Alice, Bob!"
只能使用一次:一个函数中只能有一个剩余参数:
jsfunction invalid(a, ...rest1, ...rest2) {} // 错误
与箭头函数配合:箭头函数没有
arguments
,剩余参数是唯一选择:jsconst add = (...nums) => nums.reduce((a, b) => a + b); console.log(add(1, 2, 3)); // 6
对比 arguments:
特性 | 剩余参数 | arguments 对象 |
---|---|---|
类型 | 真正的数组 | 类数组对象(需转换才能用数组方法) |
箭头函数中可用 | ✅ 是 | ❌ 箭头函数无 arguments |
可读性与灵活性 | 直接命名,语义清晰 | 无命名,需通过索引访问 |
与其他参数共存 | 可结合普通参数和解构赋值 | 只能通过索引访问所有参数 |
包含参数 | 只包含没有对应形参的实参 | 包含所有实参 |
应用场景:
处理不定数量的参数
jsfunction logMessages(...messages) { messages.forEach(msg => console.log(msg)); } logMessages("Error", "Warning", "Info"); // 逐行输出
与解构赋值结合
数组解构:收集剩余元素
jsconst [first, ...others] = [1, 2, 3, 4]; console.log(first); // 1 console.log(others); // [2, 3, 4]
对象解构:收集剩余属性
jsconst { x, y, ...rest } = { x: 1, y: 2, z: 3, a: 4 }; console.log(rest); // { z: 3, a: 4 }
替代
arguments
对象js// 旧方式(arguments) function oldSum() { const args = Array.from(arguments); return args.reduce((a, b) => a + b, 0); } // 新方式(剩余参数) function newSum(...args) { return args.reduce((a, b) => a + b, 0); }
函数对象属性
我们知道 JS 中函数也是一个对象,那么对象中就可以有属性和方法。
函数对象的属性可以分为:
- 自定义函数属性
- 内置函数属性
自定义函数属性
内置函数属性
name
function.name:string
,只读,返回函数定义时的名称,推断规则因定义方式而异。。适用于调试、反射(获取函数名)等场景。
不同函数类型的name值:
函数声明
显式命名:直接返回定义的名称。
jsfunction sum(a, b) { return a + b; } console.log(sum.name); // "sum"
匿名函数表达式
ES5 及之前:匿名函数的
name
为空字符串""
。ES6 及之后:若函数被赋值给变量,
name
会推断为变量名。jsconst multiply = function(a, b) { return a * b; }; console.log(multiply.name); // "multiply"(ES6+) const func = function() {}; console.log(func.name); // "func"
箭头函数
行为与函数表达式一致,
name
推断为变量名。jsconst divide = (a, b) => a / b; console.log(divide.name); // "divide"
构造函数(类)
返回类名,与函数声明行为一致。
jsclass Person { constructor(name) { this.name = name; } } console.log(Person.name); // "Person" const Car = function() {}; console.log(Car.name); // "Car"
对象方法
普通方法:返回方法名。
jsconst obj = { sayHello() { console.log("Hello"); } }; console.log(obj.sayHello.name); // "sayHello"
计算属性名:返回实际定义的方法名。
jsconst methodName = 'dynamicMethod'; const obj2 = { [methodName]() {} }; console.log(obj2[methodName].name); // "dynamicMethod"
Symbol属性名:返回
Symbol
的描述(需通过toString()
获取完整信息)。jsconst sym = Symbol('customSymbol') const obj = { [sym]: function () { console.log(obj[sym].name) // [customSymbol] } } obj[sym]()
动态创建的函数
通过
new Function()
:返回 "anonymous"。jsconst dynamicFunc = new Function('a', 'b', 'return a + b'); console.log(dynamicFunc.name); // "anonymous"
通过
bind()
:返回原函数名前缀加 "bound "。jsfunction greet() {} const boundGreet = greet.bind({}); console.log(boundGreet.name); // "bound greet"
Getter/Setter 方法
返回
get
或set
前缀的属性名。jsconst obj3 = { get value() { return this._value; }, set value(v) { this._value = v; } }; console.log(obj3.value.getter?.name); // "get value"(需通过对象描述符获取)
默认参数函数
返回函数本身的名称。
jsfunction defaultFunc(param = function() {}) { console.log(param.name); // "param"(非标准行为,因环境而异) } defaultFunc();
应用场景:
遍历区分存储在数组中的函数
length
function.length:number
,表示函数声明时定义的形参数量。它不会将默认参数之后的参数、剩余参数计算在内。
语法特性:
基本定义
返回函数声明时定义的形参数量,不包含默认参数之后的参数和剩余参数。
jsfunction sum(a, b, c) {} // 3
默认参数的影响
length
计算到第一个具有默认值的参数之前,后续参数无论是否有默认值均不计数。jsfunction func1(a, b = 0, c) {} // 1(a 无默认值,b 有默认值,之后参数不计) function func2(a = 0, b, c) {} // 0(第一个参数 a 有默认值)
剩余参数(Rest Parameters)
剩余参数不计入
length
,但会计算其之前的参数。jsfunction rest1(...args) {} // 0 function rest2(a, ...args) {} // 1(a 计入,...args 不计)
解构赋值参数
解构参数视为一个整体参数,不影响计数规则。
jsfunction destructure1({ x, y }, z) {} // 2(两个参数:对象解构和 z) function destructure2([a, b] = [], c) {} // 0(第一个参数有默认值)
arguments
function.arguments:类数组对象
,已废弃,用于在函数内部访问函数执行时传入的参数列表,它是一个类数组对象。
历史行为:
非严格模式:允许通过函数名.arguments
此属性获取参数。
function fn(a, b) {
console.log(fn.arguments); // Arguments(2) [1, 2, callee: ƒ, Symbol(Symbol.iterator): ƒ]
}
fn(1, 2);
严格模式:在函数内部直接通过 arguments
访问参数(非箭头函数)。
function sum(a, b) {
console.log(arguments[0], arguments[1]); // 1, 2
return a + b;
}
sum(1, 2);
转数组类型:通过 Array.from(arguments)
或[...arguments ]
将类数组的 arguments 转为数组。
function convertToArray() {
const argsArray = Array.from(arguments);
// 或 const argsArray = [...arguments];
console.log(argsArray); // [1, 2, 3]
}
convertToArray(1, 2, 3);
箭头函数不绑定 arguments:
箭头函数是不绑定 arguments 的,所以我们在箭头函数中使用 arguments 会去上层作用域查找:
箭头函数不绑定 arguments
jsconst bar = (x, y, z) => { console.log(arguments) // Uncaught ReferenceError: arguments is not defined } bar(10, 20, 30)
在箭头函数中使用 arguments 会去上层作用域查找
jsconst bar = function (m, n) { return (x, y, z) => { console.log(arguments) // Arguments(2) [10, 20, callee: ƒ, Symbol(Symbol.iterator): ƒ] } } const fn = bar(10, 20) fn('a', 'b', 'c')
废弃原因:
严格模式报错
在严格模式(
'use strict'
)下,访问function.arguments
会直接抛出错误:js'use strict'; function func() { console.log(func.arguments); // TypeError: 'caller', 'callee', and 'arguments' may not be accessed } func();
性能与安全性问题
- 引擎难以优化此属性的访问。
- 暴露函数执行上下文的细节,可能引发安全问题。
箭头函数的限制
箭头函数没有自己的
arguments
对象,也无法通过此方式访问参数:jsconst arrowFunc = () => { console.log(arrowFunc.arguments); // 报错或无输出 }; arrowFunc(1, 2);
示例:
纯函数
纯函数
函数式编程中有一个非常重要的概念叫纯函数(Pure Function),JavaScript 符合函数式编程的范式,所以也有纯函数的概念;
在react开发中纯函数是被多次提及的;
比如react 中组件就被要求像是一个纯函数(为什么是像,因为还有 class 组件),redux 中有一个 reducer 的概念,也是要求必须是一个纯函数;
所以掌握纯函数对于理解很多框架的设计是非常有帮助的;
维基百科:纯函数的维基百科定义:
在程序设计中,若一个函数符合以下条件,那么这个函数被称为纯函数:
此函数在相同的输入值时,需产生相同的输出。
函数的输出和输入值以外的其他隐藏信息或状态无关,也和由 I/O 设备产生的外部输出无关。
该函数不能有语义上可观察的函数副作用,诸如“触发事件”,使输出设备输出,或更改输出值以外物件的内容等。
纯函数(Pure Function):是函数式编程中的核心概念,它指满足以下两个条件的函数:
- 相同输入,相同输出:在给定相同的参数时,总是返回相同的结果,不受外部状态或环境变化的影响。
- 无副作用(No Side Effects):函数执行过程中不会修改任何外部状态,包括:
- 不修改全局变量、外部对象或传入的参数。
- 不执行 I/O 操作(如读写文件、网络请求、DOM 操作等)。
- 不触发外部事件(如日志输出、计时器等)。
副作用
那么这里又有一个概念,叫做副作用,什么又是副作用呢?
副作用(side effect):
其实本身是医学的一个概念,比如我们经常说吃什么药本来是为了治病,可能会产生一些其他的副作用;
在计算机科学中,也引用了副作用的概念,表示在执行一个函数时,除了返回函数值之外,还对调用函数产生了附加的影响,比如修改了全局变量,修改参数或者改变外部的存储;
纯函数在执行的过程中就是不能产生这样的副作用:
- 副作用往往是产生 bug 的 “温床”。
示例:副作用:修改了参数、外部变量
练习:判断纯函数
1、是纯函数
2、不是纯函数:访问了外部变量 foo
3、不是纯函数:修改了外部对象 info.name
数组中的纯函数
数组中的纯函数:我们来看一个对数组操作的两个函数:
- array.slice:
(start?,end?)
,纯函数,用于提取数组的一部分,返回新数组,且不修改原数组。 - array.splice():
(start,deleteCount?,item1?,item2?,...)
,用于修改原数组,可以删除、替换或添加元素。它会直接改变原数组,并返回被删除的元素组成的数组。
作用和优势
为什么纯函数在函数式编程中非常重要呢?
作用:
安心的编写和安心的使用
安心的编写:写的时候保证了函数的纯度,只是单纯实现自己的业务逻辑即可,不需要关心传入的内容是如何获得的或者依赖其他的外部变量是否已经发生了修改。
安心的使用:用的时候可以确定输入内容不会被任意篡改,并且确定的输入,一定会有确定的输出。
react 要求组件是一个纯函数
React中要求我们无论是函数还是 class 声明一个组件,这个组件都必须像纯函数一样,保护它们的 props 不被修改
编写纯函数
如何编写纯函数:
避免依赖外部变量
所有数据通过参数传递,而非直接使用全局变量。
js// 不纯(依赖外部变量) const TAX_RATE = 0.1; function calculateTax(price) { return price * TAX_RATE; } // 纯(依赖参数) function calculateTaxPure(price, taxRate) { return price * taxRate; }
不修改输入参数
对对象或数组的操作应返回新值,而非直接修改原数据。
js// 不纯(修改输入) function addToCartImpure(cart, item) { cart.push(item); return cart; } // 纯(返回新数组) function addToCartPure(cart, item) { return [...cart, item]; }
隔离副作用
将 I/O 操作、状态修改等副作用与纯逻辑分离,如通过高阶函数包装。
js// 副作用隔离:纯函数处理逻辑,非纯函数处理 I/O function logResult(impureAction) { return (...args) => { const result = impureAction(...args); console.log("Result:", result); return result; }; } const pureAdd = (a, b) => a + b; const loggedAdd = logResult(pureAdd);
柯里化
概述
柯里化(Currying):是一种函数式编程技术,将一个接受多个参数的函数转换为一系列嵌套的单参数函数。每次调用接收一个参数并返回一个新函数,直到所有参数被收集完毕,最终返回计算结果。柯里化的核心目的是增强函数的复用性和灵活性,支持部分参数应用(Partial Application)。
核心概念:
- 函数转换:将多参数函数转化为单参数函数的链式调用。如,
add(a, b, c)
柯里化为add(a)(b)(c)
。 - 延迟执行:分步传递参数,按需触发最终计算。
- 部分应用:提前固定部分参数,生成更具体的函数。
柯里化(Currying)也是属于函数式编程里面一个非常重要的概念。
是一种关于函数的高阶技术;
它不仅被用于 JavaScript,还被用于其他编程语言;
维基百科:
柯里化(Currying,卡瑞化,加里化):是把接收多个参数的函数,变成接收一个单一参数(最初函数的第一个参数)的函数,并返回接收余下的参数且返回结果的新函数的技术;
- 柯里化声称 “如果你固定某些参数,你将得到接受余下参数的一个函数”;
总结:维基百科的解释非常的抽象,我们这里做一个总结:
- 只传递给函数一部分参数来调用它,让它返回一个函数去处理剩余的参数,这个过程就称之为柯里化;
柯里化是一种函数的转换,将一个函数从可调用的 f(a, b, c) 转换为可调用的 f(a)(b)(c)。
- 柯里化不会调用函数。它只是对函数进行转换。
柯里化函数
柯里化函数:
那么柯里化到底是怎么样的表现呢?
1、普通的函数
2、柯里化函数
3、柯里化函数(箭头函数写法)
示例:
打印日志
1、普通函数实现
2、柯里化函数实现:可以发现普通函数打印日志时,前面2个参数是相同的,可以通过柯里化函数优化
工厂函数 makeAdder
柯里化的优势
柯里化的优势:
- 职责单一:一个函数处理的问题尽可能的单一。
- 参数复用:提前固定部分参数,生成专用函数(如固定税率、单位换算)。
- 函数组合:便于将多个小函数组合成复杂逻辑(如
compose(f, g)(x)
)。 - 延迟计算:分步传递参数,按需触发执行(如事件处理、条件满足后执行)。
优势:职责单一:
在函数式编程中,我们其实往往希望一个函数处理的问题尽可能的单一,而不是将一大堆的处理过程交给一个函数来处理;那么我们是否就可以将每次传入的参数在单一的函数中进行处理,处理完后在下一个函数中再使用处理后的结果;
示例:每个函数只处理一件事
上面的案例做如下修改:传入的函数需要分别被进行如下处理:
第一个参数 + 2
第二个参数 * 2
第三个参数 ** 2
优势:参数复用:
另外一个使用柯里化的场景是可以帮助复用参数逻辑:
示例:工厂函数 makeAdder
makeAdder 函数要求我们传入一个 num(并且如果我们需要的话,可以在这里对 num 进行一些修改);
在之后使用返回的函数时,我们不需要再继续传入 num 了;
手写自动柯里化函数@
目前我们有将多个普通的函数,转成柯里化函数:
组合函数
组合函数概念的理解
组合函数(Compose Function):是在 JavaScript 开发过程中一种对函数的使用技巧、模式:
比如我们现在需要对某一个数据进行函数的调用,执行两个函数 fn1 和 fn2,这两个函数是依次执行的;
那么如果每次我们都需要进行两个函数的调用,操作上就会显得重复;
那么是否可以将这两个函数组合起来,自动依次调用呢?
这个过程就是对函数的组合,我们称之为组合函数;
手写组合函数@
刚才我们实现的 compose 函数比较简单
我们需要考虑更加复杂的情况:比如传入了更多的函数,在调用 compose 函数时,传入了更多的参数:
with、eval
with语句的使用
with语句扩展一个语句的作用域链。
不建议使用 with 语句,因为它可能是混淆错误和兼容性问题的根源。
eval函数
内建函数 eval 允许执行一个代码字符串。
eval 是一个特殊的函数,它可以将传入的字符串当做 JavaScript 代码来运行;
eval 会将最后一句执行语句的结果,作为返回值;
不建议在开发中使用 eval:
eval 代码的可读性非常的差(代码的可读性是高质量代码的重要原则);
eval 是一个字符串,那么有可能在执行的过程中被刻意篡改,那么可能会造成被攻击的风险;
eval 的执行必须经过 JavaScript 解释器,不能被 JavaScript 引擎优化;
严格模式
认识严格模式
JavaScript 历史的局限性:
长久以来,JavaScript 不断向前发展且并未带来任何兼容性问题;
新的特性被加入,旧的功能也没有改变,这么做有利于兼容旧代码;
但缺点是 JavaScript 创造者的任何错误或不完善的决定也将永远被保留在 JavaScript 语言中;
在 ECMAScript5 标准中,JavaScript 提出了 严格模式(Strict Mode) 的概念:
严格模式很好理解,是一种具有限制性的 JavaScript 模式,从而使代码隐式的脱离了 ”懒散(sloppy)模式“;
支持严格模式的浏览器在检测到代码中有严格模式时,会以更加严格的方式对代码进行检测和执行;
严格模式对正常的 JavaScript 语义进行了一些限制:
严格模式通过 抛出错误 来消除一些原有的静默(silent)错误;
严格模式让JS 引擎在执行代码时可以进行更多的优化(不需要对一些特殊的语法进行处理);
严格模式禁用了在ECMAScript 未来版本中可能会定义的一些语法;
开启严格模式
那么如何开启严格模式呢?严格模式支持粒度化迁移:
可以支持在js 文件中开启严格模式;
也支持对某一个函数开启严格模式;
严格模式通过在文件或者函数开头使用 use strict 来开启。
注意:
没有类似于 "no use strict" 这样的指令可以使程序返回默认模式。
现代 JavaScript 支持 “class” 和 “module” ,它们会自动启用 use strict;
严格模式限制
JavaScript 被设计为新手开发者更容易上手,所以有时候本来错误语法,被认为也是可以正常被解析的;但是这种方式可能给带来留下来安全隐患;在严格模式下,这种失误就会被当做错误,以便可以快速的发现和修正;
严格模式限制:这里我们来说几个严格模式下的严格语法限制:
1、无法意外的创建全局变量
2、严格模式会使引起静默失败(silently fail,注:不报错也没有任何效果)的赋值操作抛出异常
3、严格模式下试图删除不可删除的属性
4、严格模式不允许函数参数有相同的名称
5、不允许 0 的八进制语法,要使用 0o
6、在严格模式下,不允许使用 with
7、在严格模式下,eval 不能为上层引用(创建)变量
8、严格模式下,this 绑定不会默认转成对象,也不会绑定 window,而是 undefined
手写apply、call、bind函数实现(原型后)
接下来我们来实现一下 apply、call、bind 函数:
- 注意:我们的实现是练习函数、this、调用关系,不会过度考虑一些边界情况
手写
手写 call,aplly,bind
函数对象原型关系
函数 foo 对象的隐式原型 === Function 的显式原型
// 函数foo对象的隐式原型 === Function的显式原型
console.log(foo.__proto__ === Function.prototype); // true
console.log(Function.prototype.apply); // f apply()
console.log(Function.prototype.call); // f call()
console.log(Function.prototype.bind); // f bind()
console.log(Function.prototype.apply === foo.apply); // true
结论:
- foo对象中的某些属性和方法是来自 Function.prototype 的
- 在 Function.prototype 中添加的属性和方法,可以被所有的函数获取
在 Function 的原型中添加方法 bar
手写 apply 方法
给函数对象添加方法
function foo() {
console.log("foo", this);
}
Function.prototype.mrapply = function (mrthis) {
// 相当于 mrthis.fn = this
Object.defineProperty(mrthis, "fn", {
configurable: true,
value: this,
});
// 隐式调用fn,可以让fn函数的this指向 mrthis
mrthis.fn();
// 删除多出来的临时函数fn
delete mrthis.fn;
};
foo.mrapply({ name: "Tom" });
如果传入的参数是一个 String 或者 Number 的类型,需要将其包裹成对象类型,才能在它上面添加属性
调用 mrapply 时,传递参数
function foo (age, height) {
console.log('foo', this, age, height)
}
+ Function.prototype.mrapply = function(mrthis, args) {
// 当this不是对象时,需要用Object包裹
mrthis = (mrthis === null || mrthis === undefined) ? window : Object(mrthis)
// 相当于 mrthis.fn = this
Object.defineProperty(mrthis, 'fn', {
configurable: true,
value: this
})
// 隐式调用fn,可以让fn函数的this指向 mrthis
+ mrthis.fn(...args)
// 删除多出来的临时函数fn
delete mrthis.fn
}
+ foo.mrapply({name: "Tom"}, [18, 1.88])
foo.mrapply(null, [18, 1.88])
foo.mrapply(undefined, [18, 1.88])
foo.mrapply(true, [18, 1.88])
foo.mrapply(123, [18, 1.88])
foo.mrapply('aaaa', [18, 1.88])
手写 call 方法
function foo(age, height) {
console.log('foo', this, age, height)
}
+ Function.prototype.mrcall = function(mrthis, ...args) {
mrthis = (mrthis === null || mrthis === undefined) ? window : Object(mrthis)
Object.defineProperty(mrthis, 'fn', {
configurable: true,
value: this
})
+ mrthis.fn(...args)
delete mrthis.fn
}
+ foo.mrcall({ name: "张飞" }, 20, 1.77)
抽取封装公共函数
/* 抽取封装的函数 */
+ Function.prototype.mrexec = function(mrthis, args) {
mrthis = (mrthis === null || mrthis === undefined) ? window : Object(mrthis)
// mrthis.fn = this
Object.defineProperty(mrthis, 'fn', {
configurable: true,
value: this
})
mrthis.fn(...args)
delete mrthis.fn
}
/* 手写apply */
Function.prototype.mrapply = function(mrthis, args) {
this.mrexec(mrthis, args)
}
/* 手写call */
Function.prototype.mrcall = function(mrthis, ...args) {
this.mrexec(mrthis, args)
}
// 测试
function foo(age, height) {
console.log('foo', this, age, height)
}
foo.mrapply({name: "Tom"}, [19, 1.66])
foo.mrcall({name: "Jack"}, 22, 1.99)
手写 bind 方法
和 apply, call 不同,bind 执行后是返回一个新的函数 newFoo
基础实现
思路:想办法实现如下:
// 伪代码
{ name: "why" }.foo(name, age)
/* 手写bind */
Function.prototype.mrbind = function(mrthis, ...args) {
+ return (...moreArgs) => {
mrthis = (mrthis === null || mrthis === undefined) ? window : Object(mrthis)
Object.defineProperty(mrthis, 'fn', {
configurable: true,
value: this
})
+ const allArgs = [...args, ...moreArgs]
+ mrthis.fn(...allArgs)
+ delete mrthis.fn // 可以删除fn,因为每次调用newFoo,都会重新生成一个mrthis.fn
}
}
// 测试
function foo(name, age, height, address) {
console.log('foo', this, name, age, height, address)
}
const newFoo = foo.mrbind({name: "Jerry"}, '张飞', 45)
console.log(newFoo)
+ newFoo(1.88, '成都')
+ newFoo(1.88, '成都')
浅拷贝,深拷贝
引用赋值
浅拷贝
方式:
- 解构赋值:
const info = {...obj}
浅拷贝修改 info2.name 后,obj 的 name 依然是"why",被修改的只是 info2
浅拷贝的内存图
如果 obj 对象中有**其他对象(或数组)**时的内存图
深拷贝
方式:
- 1、借助第三方库:
underscore
- 2、利用现有 JS 机制:
JSON
- 3、自己实现:
2、利用现有 JS 机制:JSON
语法:
const info3 = JSON.parse(JSON.stringify(obj));
缺点: 该方法不能实现方法的深拷贝,会忽略 obj 对象中的方法
const obj = {
name: 'Tom',
age: 18,
friend: {
name: 'Jack'
},
run: function() {
console.log(this.name + '在跑步~');
}
}
// 利用JSON机制实现深拷贝
+ const info = JSON.parse(JSON.stringify(obj))
// 测试
console.log(info)
// 修改info的深度属性,obj的深度属性保持不变
+ info.friend.name = '张飞'
+ console.log('obj', obj.friend.name); // obj Jack
+ console.log('info', info.friend.name); // obj 张飞
// 不能实现方法的深拷贝,会忽略obj对象中的方法
+ info.run() // ncaught TypeError: info.run is not a function
防抖、节流
简介
防抖和节流的概念其实最早并不是出现在软件工程中,防抖是出现在电子元件中,节流出现在流体流动中
而JavaScript是事件驱动的,大量的操作会触发事件,加入到事件队列中处理。
而对于某些频繁的事件处理会造成性能的损耗,我们就可以通过防抖和节流来限制事件频繁的发生;
防抖和节流函数目前已经是前端实际开发中两个非常重要的函数,也是面试经常被问到的面试题。
但是很多前端开发者面对这两个功能,有点摸不着头脑:
某些开发者根本无法区分防抖和节流有什么区别(面试经常会被问到);
某些开发者可以区分,但是不知道如何应用;
某些开发者会通过一些第三方库来使用,但是不知道内部原理,更不会编写;
接下来我们会一起来学习防抖和节流函数:
我们不仅仅要区分清楚防抖和节流两者的区别,也要明白在实际工作中哪些场景会用到;
并且我会带着大家一点点来编写一个自己的防抖和节流的函数,不仅理解原理,也学会自己来编写;
防抖函数
防抖函数(debounce)
我们用一副图来理解一下它的过程:
当事件触发时,相应的函数并不会立即触发,而是会等待一定的时间;
当事件密集触发时,函数的触发会被频繁的推迟;
只有等待了一段时间也没有事件触发,才会真正的执行响应函数;
应用场景:
防抖的应用场景很多:
搜索联想:
oninput
,输入框中频繁的输入内容,搜索或者提交信息;频繁点击事件:
onclick
,频繁的点击按钮,触发某个事件;浏览器滚动事件:
onscroll
,监听浏览器滚动事件,完成某些特定操作;浏览器缩放事件:
onresize
,用户缩放浏览器的resize事件;
示例: 搜索联想
我们都遇到过这样的场景,在某个搜索框中输入自己想要搜索的内容:
比如想要搜索一个MacBook:
当我输入m时,为了更好的用户体验,通常会出现对应的联想内容,这些联想内容通常是保存在服务器的,所以需要一次网络请求;
当继续输入ma时,再次发送网络请求;
那么macbook一共需要发送7次网络请求;
这大大损耗我们整个系统的性能,无论是前端的事件处理,还是对于服务器的压力;
但是我们需要这么多次的网络请求吗?
不需要,正确的做法应该是在合适的情况下再发送网络请求;
比如如果用户快速的输入一个macbook,那么只是发送一次网络请求;
比如如果用户是输入一个m想了一会儿,这个时候m确实应该发送一次网络请求;
也就是我们应该监听用户在某个时间,比如500ms内,没有再次触发时间时,再发送网络请求;
这就是防抖的操作:只有在某个时间内,没有再次触发某个函数时,才真正的调用这个函数;
节流函数
节流函数(throttle)
我们用一副图来理解一下节流的过程
当事件触发时,会执行这个事件的响应函数;
如果这个事件会被频繁触发,那么节流函数会按照一定的频率来执行函数;
不管在这个中间有多少次触发这个事件,执行函数的频率总是固定的;
应用场景:
页面滚动事件:监听页面的滚动事件;
鼠标移动事件;
频繁点击事件:用户频繁点击按钮操作;
游戏某些设计:游戏中的一些设计,如发射子弹;
很多人都玩过类似于飞机大战的游戏
在飞机大战的游戏中,我们按下空格会发射一个子弹:
很多飞机大战的游戏中会有这样的设定,即使按下的频率非常快,子弹也会保持一定的频率来发射;
比如1秒钟只能发射一次,即使用户在这1秒钟按下了10次,子弹会保持发射一颗的频率来发射;
但是事件是触发了10次的,响应的函数只触发了一次;
生活中的例子
生活中防抖的例子:
比如说有一天我上完课,我说大家有什么问题来问我,我会等待五分钟的时间。
如果在五分钟的时间内,没有同学问我问题,那么我就下课了;
在此期间,a同学过来问问题,并且帮他解答,解答完后,我会再次等待五分钟的时间看有没有其他同学问问题;
如果我等待超过了5分钟,就点击了下课(才真正执行这个时间);
生活中节流的例子:
比如说有一天我上完课,我说大家有什么问题来问我,但是在一个5分钟之内,不管有多少同学来问问题,我只会解答一个问题;
如果在解答完一个问题后,5分钟之后还没有同学问问题,那么就下课;
案例准备
我们通过一个搜索框来延迟防抖函数的实现过程:
- 监听input的输入,通过打印模拟网络请求
测试发现快速输入一个macbook共发送了7次请求,显示我们需要对它进行防抖操作:
underscore
Underscore库的介绍
事实上我们可以通过一些第三方库来实现防抖操作:
lodash
underscore
这里使用underscore
我们可以理解成lodash是underscore的升级版,它更重量级,功能也更多;
但是目前我看到underscore还在维护,lodash已经很久没有更新了;
Underscore的官网: https://underscorejs.org/
安装:
Underscore的安装有很多种方式:
下载Underscore,本地引入;
通过CDN直接引入;
通过包管理工具(npm)管理安装;
这里我们直接通过CDN:
<script src="https://cdn.jsdelivr.net/npm/underscore@1.13.1/underscore-umd-min.js"></script>
Underscore实现防抖和节流
手写题
手写-防抖函数
我们按照如下思路来实现:
- 防抖基本功能实现:可以实现防抖效果
- 优化一:优化参数和this指向
- 优化二:优化取消操作(增加取消功能)
- 优化三:优化立即执行效果(第一次立即执行)
- 优化四:优化返回值
1、基本实现
2、优化:参数和this绑定
this指向
参数
3、优化:取消功能
4、优化:第一次立即执行
immediate
:控制否时启用立即执行功能isInvoke
:控制函数是否已经立即执行一次了
5、优化:返回值
手写-节流函数
我们按照如下思路来实现:
- 节流函数的基本实现:可以实现节流效果
- 优化一:绑定this和参数
- 优化二:控制立即执行,节流最后一次也可以执行
- 优化三:优化添加取消功能
- 优化四:优化返回值问题
1、基本实现
2、优化:绑定this和参数
3、优化:控制立即执行
4、优化:控制执行最后一次
思路一: 给每次点击时添加一个定时器,延迟时间设为waitTime,当再次点击时取消上次的定时器,重新添加一个。
思路二: 在每个执行fn函数的节点,添加一个延迟时间为waitTime的定时器,当用户在fn函数执行节点的时间上也点击了一次就取消该定时器(使用中)
4、优化:取消功能
5、优化:返回值
手写-深拷贝函数
前面我们已经学习了对象相互赋值的一些关系,分别包括:
引用赋值:指向同一个对象,相互之间会影响;
对象的浅拷贝:只是浅层的拷贝,内部引入对象时,依然会相互影响;
对象的深拷贝:两个对象不再有任何关系,不会相互影响;
深拷贝实现方式:
- JSON.parse
- 第三方库:underscore、lodash
- 自己实现
前面我们已经可以通过一种方法来实现深拷贝了:JSON.parse
这种深拷贝的方式其实对于函数、Symbol等是无法处理的;
并且如果存在对象的循环引用,也会报错的;
const obj = JSON.parse(JSON.stringify(info))
自定义深拷贝函数:
1.自定义深拷贝的基本功能;
2.对Symbol的key进行处理;
3.其他数据类型的值进程处理:数组、函数、Symbol、Set、Map;
4.对循环引用的处理;
工具函数:判断对象
1、基本实现
2、优化:区分数组和对象
3、优化:其他类型-处理set
4、优化:其他类型-处理map
5、优化:其他类型-处理function
function: 不需要深拷贝
6、优化:其他类型-处理Symbol为值
7、优化:其他类型-处理Symbol为key
8、优化:处理循环引用
方案一:将每次新创建的对象保存到Map中,每次遍历前判断之前是否已经保存过了该对象
问题:需要在deeCopy外部定义一个map,并且每次拷贝完成后map依然会形成对对象的强引用,没有销毁
方案二(推荐):使用WeakMap替代Map;将map放入参数中并设置一个默认值new WeakMap()
手写-事件总线
自定义事件总线属于一种观察者模式,其中包括三个角色:
发布者(Publisher):发出事件(Event);
订阅者(Subscriber):订阅事件(Event),并且会进行响应(Handler);
事件总线(EventBus):无论是发布者还是订阅者都是通过事件总线作为中台的;
当然我们可以选择一些第三方库:
Vue2默认是带有事件总线的功能;
Vue3中推荐一些第三方库,比如mitt;
当然我们也可以实现自己的事件总线:
事件的监听方法on;
事件的发射方法emit;
事件的取消监听off;
1、基本实现
2、优化:绑定参数
3、优化:移除监听