总结:
-
函数式编程其实是一种编程思想,它追求更细的粒度,将应用拆分成一组组极小的单元函数,组合调用操作数据流;
-
它提倡着 纯函数 / 函数复合 / 数据不可变, 谨慎对待函数内的 状态共享 / 依赖外部 / 副作用;
开源分享:【大厂前端面试题解析+核心总结学习笔记+真实项目实战+最新讲解视频】
Tips:
其实我们很难也不需要在面试过程中去完美地阐述出整套思想,这里也只是浅尝辄止,一些个人理解而已。博主也是初级小菜鸟,停留在表面而已,只求对大家能有所帮助,轻喷🤣;
我个人觉得: 这些编程范式之间,其实并不矛盾,各有各的 优劣势。
理解和学习它们的理念与优势,合理地 设计融合,将优秀的软件编程思想用于提升我们应用;
所有设计思想,最终的目标一定是使我们的应用更加 解耦颗粒化、易拓展、易测试、高复用,开发更为高效和安全;
$(location).attr(‘href’, ‘http://stackoverflow.com’)
闭包是一个函数和对该函数外部作用域的引用(词法环境),词法环境是每个执行上下文(堆栈)的一部分,并且是标识符(即局部变量名称)和值之间的映射。
JavaScript 中的每个函数都维护对其外部词法环境的引用。此引用用于配置调用函数时创建的执行上下文。不管何时调用函数,该引用使函数内的代码能够查看在函数外声明的变量。
在下面的代码中,inner
与调用foo
时创建的执行上下文的词法环境一起形成一个闭包,并对外部隐藏了变量secret
:
function foo() {
const secret = Math.trunc(Math.random()*100)
return function inner() {
console.log(The secret number is ${secret}.
)
}
}
const f = foo() // secret 不能从foo 外部直接访问
f() // 访问 secret 的唯一办法就是调用 f
换句话说,在JavaScript中,函数带有对私有状态的引用,只有它们(以及在相同的词法环境中声明的任何其他函数)可以访问该私有状态。这个状态对函数的调用者是不可见的,这为数据隐藏和封装提供了一种优秀的机制。
请记住,JavaScript中的函数可以像变量一样传递,这意味着这些功能和状态的对可以在程序中传递:类似于在c++中传递类的实例。
如果JavaScript没有闭包,则必须在函数之间显式传递更多状态,从而使参数列表更长,代码更冗余。
所以,如果你想让一个函数总是能够访问私有状态,你可以使用一个闭包,我们经常想把状态和函数联系起来。例如,在Java或c++中,当你向类添加私有实例变量和方法时,这是将状态与功能关联起来。
在 C 语言和大多数其他编程语言中,函数返回后,由于堆栈被销毁,所有的局部变量都不再可访问。在JavaScript中,如果在另一个函数中声明一个函数,那么外部函数的本地变量在返回后仍然可以访问。这样,在上面的代码中,secret
在从foo
返回后仍然对函数对象内部可用。
闭包在需要与函数关联的私有状态时非常有用。这是一个非常常见的场景,JavaScript直到2015年才有类语法,它仍然没有私有字段语法,闭包满足了这一需求。
私有实例变量
在下面的事例中,函数 toString
隐藏了 Car 类的一些细节。
function Car(manufacturer, model, year, color) {
return {
toString() {
return ${manufacturer} ${model} (${year}, ${color})
}
}
}
const car = new Car(‘Aston Martin’,‘V8 Vantage’,‘2012’,‘Quantum Silver’)
console.log(car.toString())
函数式编程
在下面的代码中,函数inner
隐藏了fn
和args
。
function curry(fn) {
const args = []
return function inner(arg) {
if(args.length === fn.length) return fn(…args)
args.push(arg)
return inner
}
}
function add(a, b) {
return a + b
}
const curriedAdd = curry(add)
console.log(curriedAdd(2)(3)()) // 5
面向事件的编程
在以下代码中,函数onClick
隐藏了变量BACKGROUND_COLOR
。
const $ = document.querySelector.bind(document)
const BACKGROUND_COLOR = ‘rgba(200,200,242,1)’
function onClick() {
$(‘body’).style.background = BACKGROUND_COLOR
}
$(‘button’).addEventListener(‘click’, onClick)
Set background color
模块化
在下面的示例中,所有实现细节都隐藏在一个立即执行的函数表达式中。函数tick
和toString
隐藏了私有状态和函数,它们需要完成自己的工作。闭包使我们能够模块化和封装我们的代码。
let namespace = {};
(function foo(n) {
let numbers = []
function format(n) {
return Math.trunc(n)
}
function tick() {
numbers.push(Math.random() * 100)
}
function toString() {
return numbers.map(format)
}
n.counter = {
tick,
toString
}
}(namespace))
const counter = namespace.counter
counter.tick()
counter.tick()
console.log(counter.toString())
事例 1:
此示例演示局部变量未在闭包中复制。 闭包保留对原始变量本身的引用。 似乎即使外部函数退出后,堆栈仍在内存中保留。
function foo () {
let x = 42
let inner = function () {
console.log(x)
}
x = x + 1
return inner
}
let f = foo()
f()
事例 2:
在下面的代码中,三种方法log
,increment
和update
都在同一词法环境闭包中。
function createObject() {
let x = 42;
return {
log() { console.log(x) },
increment() { x++ },
update(value) { x = value }
}
}
const o = createObject()
o.increment()
o.log() // 43
o.update(5)
o.log() // 5
const p = createObject()
p.log() // 42
事例 3:
如果使用的变量是使用var
声明的,需要注意的一点是,使用var
声明的变量被提升。 由于引入了let
和cons
t,这在现代JavaScript 中几乎没有问题。
在下面的代码中,每次循环中,都会创建一个新的inner
函数,变量i
被覆盖,但是因var
会让 i
提升到函数的顶部,所以所有这些inner
函数覆盖的都是同一个变量,这意味着i(3)
的最终值被打印了三次。
function foo () {
var result = []
for (var i = 0; i < 3; i++) {
result.push(function inner () {
console.log(i)
})
}
return result
}
const result = foo()
for(var i = 0; i < 3; i++) {
resulti
}
// 3 3 3
最后一点:
-
每当在JavaScript中声明函数时,都会创建一个闭包。
-
从一个函数内部返回另一个函数是闭包的经典例子,因为外部函数内部的状态对于返回的内部函数是隐式可用的,即使外部函数已经完成执行。
-
只要在函数内使用
eval()
,就会使用一个闭包。eval
的文本可以引用函数的局部变量,在非严格模式下,甚至可以通过使用eval('var foo = ')
创建新的局部变量。 -
当在函数内部使用
new Function()
(Function constructor)时,它不会覆盖其词法环境,而是覆盖全局上下文。新函数不能引用外部函数的局部变量。 -
在JavaScript中,闭包类似于在函数声明时保留对作用域的引用(而不是复制),后者又保留对其外部作用域的引用,以此类推,一直到作用域链顶端的全局对象。
-
声明函数时创建一个闭包。 当调用函数时,此闭包用于配置执行上下文。
-
每次调用函数时都会创建一组新的局部变量。
JavaScript 中的每个函数都维护与其外部词法环境的链接。 词法环境是所有名称的映射(例如,变量,参数)及其范围内的值。因此,只要看到function
关键字,函数内部的代码就可以访问在函数外部声明的变量。
function foo(x) {
var tmp = 3;
function bar(y) {
console.log(x + y + (++tmp)); // 16
}
bar(10);
}
foo(2);
上面输出结果是16
,参数x
和变量tmp
都存在于外部函数foo
的词法环境中。函数bar
及其与函数foo
的词法环境的链接是一个闭包。
函数不必返回即可创建闭包。 仅仅凭借其声明,每个函数都会在其封闭的词法环境中关闭,从而形成一个闭包。
function foo(x) {
var tmp = 3;
return function (y) {
console.log(x + y + (++tmp)); // 16
}
}
var bar = foo(2);
bar(10); // 16
bar(10); // 17
上面还是打印16
,因为bar
内的代码仍然可以引用参数x
和变量tmp
,即使它们不再直接的作用域内。
但是,由于tmp
仍然在bar
的闭包内部徘徊,因此可以对其进行递增。 每次调用bar时,它将增加1
。
闭包最简单的例子是这样的:
var a = 10;
function test() {
console.log(a); // will output 10
console.log(b); // will output 6
}
var b = 6;
test();
当调用一个JavaScript函数时,将创建一个新的执行上下文ec
。连同函数参数和目标对象,这个执行上下文还接收到调用执行上下文的词法环境的链接,这意味着在外部词法环境中声明的变量(在上面的例子中,a
和b
)都可以从ec
获得。
每个函数都会创建一个闭包,因为每个函数都有与其外部词法环境的链接。
注意,变量本身在闭包中是可见的,而不是副本。
4. use strict 在 JavaScript 中做了什么,背后的原因是什么
引用一些有趣的部分:
严格模式是ECMAScript 5中的一个新特性,它允许我们将程序或函数放置在严格的操作上下文中。这种严格的上下文会防止某些操作被执行,并引发更多异常。
严格模式在很多方面都有帮助:
-
它捕获了一些常见的编码漏洞,并抛出异常。
-
当采取相对不安全的操作(例如访问全局对象)时,它可以防止错误或抛出错误。
-
它禁用令人困惑或考虑不周到的特性。
另外,请注意,我信可以将“strict mode”
应用于整个文件,也可以仅将其用于特定函数。
// Non-strict code…
(function(){
“use strict”;
// Define your library strictly…
})();
// Non-strict code…
如果是在混合使用旧代码和新代码的情况,这可能会有所帮助。它有点像在Perl中使用的“use strict”。通过检测更多可能导致损坏的东西,帮助我们减少更多的错误。
现在所有主流浏览器都支持严格模式。
在原生ECMAScript模块(带有import
和export
语句)和ES6类中,严格模式始终是启用的,不能禁用。
ECMAScript 6 引入了string .prototype.include
const string = “foo”;
const substring = “oo”;
console.log(string.includes(substring));
不过,IE 不支持 includes
。在 CMAScript 5或更早的环境中,使用String.prototype.indexOf
。如果找不到子字符串,则返回-1
:
var string = “foo”;
var substring = “oo”;
console.log(string.indexOf(substring) !== -1);
为了使其在旧的浏览器中运行,可以使用这种polyfill
:
if (!String.prototype.includes) {
String.prototype.includes = function(search, start) {
‘use strict’;
if (typeof start !== ‘number’) {
start = 0;
}
if (start + search.length > this.length) {
return false;
} else {
return this.indexOf(search, start) !== -1;
}
};
}
6. var functionName = function() {} 与 function functionName() {}
不同之处在于functionOne
是一个函数表达式,因此只在到达这一行时才会定义,而functionTwo
是一个函数声明,在它周围的函数或脚本被执行(由于提升)时就定义。
如,函数表达式
// TypeError: functionOne is not a function
functionOne();
var functionOne = function() {
console.log(“Hello!”);
};
函数声明:
// “Hello!”
functionTwo();
function functionTwo() {
console.log(“Hello!”);
}
过去,在不同的浏览器之间,在块中定义的函数声明的处理是不一致的。严格模式(在ES5中引入)解决了这个问题,它将函数声明的范围限定在其封闭的块上。
‘use strict’;
{ // note this block!
function functionThree() {
console.log(“Hello!”);
}
}
functionThree(); // ReferenceError
function abc(){}
也具有作用域-名称abc
在遇到该定义的作用域中定义。 例:
function xyz(){
function abc(){};
// abc 在这里定义…
}
// …不是在这里
如果想在所有浏览器上给函数起别名,可以这么做:
function abc(){};
var xyz = abc;
在本例中,xyz和abc都是同一个对象的别名
console.log(xyz === abc) // true
它的名称是自动分配的。但是当你定义它的时候
var abc = function(){};
console.log(abc.name); // “”
它的name
称为空,我们创建了一个匿名函数并将其分配给某个变量。使用组合样式的另一个很好的理由是使用简短的内部名称来引用自身,同时为外部用户提供一个长而不会冲突的名称:
// 假设 really.long.external.scoped 为 {}
really.long.external.scoped.name = function shortcut(n){
// 它递归地调用自己:
shortcut(n - 1);
// …
// 让它自己作为回调传递::
someFunction(shortcut);
// …
}
在上面的例子中,我们可以对外部名称进行同样的操作,但是这样做太笨拙了(而且速度更慢)。另一种引用自身的方法是arguments.callee
,这种写法也相对较长,并且在严格模式中不受支持。
实际上,JavaScript对待这两个语句是不同的。下面是一个函数声明:
function abc(){}
这里的abc
可以定义在当前作用域的任何地方:
// 我们可以在这里调用
abc();
// 在这里定义
function abc(){}
// 也可以在这里调用
abc();
此外,尽管有 return
语句,也可以提升:
// 我们可以在这里调用
abc();
return;
function abc(){}
下面是一个函数表达式:
var xyz = function(){};
这里的xyz
是从赋值点开始定义的:
// 我们不可以在这里调用
xyz();
// 在这里定义 xyz
xyz = function(){}
// 我们可以在这里调用
xyz();
函数声明与函数表达式之间存在差异的真正原因。
var xyz = function abc(){};
console.log(xyz.name); // “abc”
就个人而言,我们更喜欢使用函数表达式声明,因为这样可以控制可见性。当我们像这样定义函数时:
var abc = function(){};
我们知道,如果我们没有在作用域链的任何地方定义abc
,那么我们是在全局作用域内定义的。即使在eval()
内部使用,这种类型的定义也具有弹性。而定义:
function abc(){};
取决于上下文,并且可能让你猜测它的实际定义位置,特别是在eval()
的情况下,—取决于浏览器。
我们可以这样删除对象的属性:
delete myObject.regex;
// 或者
delete myObject[‘regex’];
// 或者
var prop = “regex”;
delete myObject[prop];
事例:
var myObject = {
“ircEvent”: “PRIVMSG”,
“method”: “newURI”,
“regex”: “^http://.*”
};
delete myObject.regex;
console.log(myObject);
JavaScript 中的对象可以看作键和值之间的映射。delete
操作符用于一次删除一个键(通常称为对象属性)。
var obj = {
myProperty: 1
}
console.log(obj.hasOwnProperty(‘myProperty’)) // true
delete obj.myProperty
console.log(obj.hasOwnProperty(‘myProperty’)) // false
delete
操作符不是直接释放内存,它不同于简单地将null
或undefined
值赋给属性,而是将属性本身从对象中删除。
注意,如果已删除属性的值是引用类型(对象),而程序的另一部分仍然持有对该对象的引用,那么该对象当然不会被垃圾收集,直到对它的所有引用都消失。
delete
只对其描述符标记为configurable
的属性有效。
8. JS 的比较中应使用哪个等于运算符(== vs ===)?
严格相等运算符(===
)的行为与抽象相等运算符(==
)相同,除非不进行类型转换,而且类型必须相同才能被认为是相等的。
==
运算符会进行类型转换后比较相等性。 ===
运算符不会进行转换,因此如果两个值的类型不同,则===
只会返回false。
JavaScript有两组相等运算符:===
和!==
,以及它们的孪生兄弟==
和!=
。如果这两个操作数具有相同的类型和相同的值,那么===
的结果就是 true
,而!==
的结果就是 false
。
下面是一些事例:
‘’ == ‘0’ // false
0 == ‘’ // true
0 == ‘0’ // true
false == ‘false’ // false
false == ‘0’ // true
false == undefined // false
false == null // false
null == undefined // true
’ \t\r\n ’ == 0 // true
上面有些看起来会挺困惑的,所以尽量还是使用严格比较运算符(===
)。对于引用类型,==
和===
操作一致(特殊情况除外)。
var a = [1,2,3];
var b = [1,2,3];
var c = { x: 1, y: 2 };
var d = { x: 1, y: 2 };
var e = “text”;
var f = “te” + “xt”;
a == b // false
a === b // false
c == d // false
c === d // false
总结
我在成长过程中也是一路摸爬滚打,没有任何人的指点,所以走的很艰难。例如在大三的时候,如果有个学长可以阶段性的指点一二,如果有已经工作的师兄可以告诉我工作上需要什么,我应该前面的三年可以缩短一半;后来去面试bat,失败了有5、6次,每次也不知道具体是什么原因,都是靠面试回忆去猜测可能是哪方面的问题,回来学习和完善,当你真正去招人的时候,你就会知道面试记录是多么重要,面试官可以从面试记录里看到你的成长,总是去面试,总是没有成长,就会被定义为缺乏潜力。
开源分享:【大厂前端面试题解析+核心总结学习笔记+真实项目实战+最新讲解视频】
符不会进行转换,因此如果两个值的类型不同,则===
只会返回false。
JavaScript有两组相等运算符:===
和!==
,以及它们的孪生兄弟==
和!=
。如果这两个操作数具有相同的类型和相同的值,那么===
的结果就是 true
,而!==
的结果就是 false
。
下面是一些事例:
‘’ == ‘0’ // false
0 == ‘’ // true
0 == ‘0’ // true
false == ‘false’ // false
false == ‘0’ // true
false == undefined // false
false == null // false
null == undefined // true
’ \t\r\n ’ == 0 // true
上面有些看起来会挺困惑的,所以尽量还是使用严格比较运算符(===
)。对于引用类型,==
和===
操作一致(特殊情况除外)。
var a = [1,2,3];
var b = [1,2,3];
var c = { x: 1, y: 2 };
var d = { x: 1, y: 2 };
var e = “text”;
var f = “te” + “xt”;
a == b // false
a === b // false
c == d // false
c === d // false
总结
我在成长过程中也是一路摸爬滚打,没有任何人的指点,所以走的很艰难。例如在大三的时候,如果有个学长可以阶段性的指点一二,如果有已经工作的师兄可以告诉我工作上需要什么,我应该前面的三年可以缩短一半;后来去面试bat,失败了有5、6次,每次也不知道具体是什么原因,都是靠面试回忆去猜测可能是哪方面的问题,回来学习和完善,当你真正去招人的时候,你就会知道面试记录是多么重要,面试官可以从面试记录里看到你的成长,总是去面试,总是没有成长,就会被定义为缺乏潜力。
开源分享:【大厂前端面试题解析+核心总结学习笔记+真实项目实战+最新讲解视频】
[外链图片转存中…(img-CYRu3b6G-1715246990290)]
[外链图片转存中…(img-L8qlzXJk-1715246990292)]