无论是业务开发还是底层开发,面向对象还是面向过程,函数始终是我们绕不开并且接触非常频繁的一个点,之前原型链一章中有简单提到过Function,这节专门拿出来整理一下这块的知识,用典型的大厂面试题加MDN官方文档解读的方式重温你不知道的Function

函数申明与函数表达式

1
!function(){alert('ghostwang')}() //true

image-20210308075639596

以上代码返回执行后结果返回true应该很好理解,因为这个匿名函数没有返回值,所以为默认返回的是undefined,感叹号(非)取反后自然就是true`。问题不在于返回值,而是取反操作为什么能让一个匿名函数自调用变得合法?

我相信就算是初级前端在自学函数的时候都有听到过匿名函数自调用的解决方案,但是这种场景在业务开发中真的很少见,一般都会用函数表达式,特别是在运用框架之后,对原生javascript的操作频率更加低,大部分知道的可能是用括号把匿名函数包装起来,如下方式:

1
2
3
(function(){alert('ghostwang')})()        // true
// 或者
(function(){alert('ghostwang')}()) // true

虽然括号位置不一样,但是执行效果以及结果一模一样。

但是越来越频繁的发现很多人会使用!来获得相同的答案,难道是为了节约一个字符吗?好像并不现实,如果不是为了体积考虑,那猜测可能是为了性能上的提升?先打个问号。

其实无论是括号,还是感叹号,让整个语句合法做的事情只有一件,就是让一个函数声明语句变成了一个表达式

1
function foo(){alert('ghostwang')} // undefined

这是一个函数声明,如果在这么一个声明后直接加上括号调用,解析器自然不会理解而报错:

1
function foo(){alert('ghostwang')}() // SyntaxError: unexpected_token

因为这样的代码混淆了函数声明和函数调用,以这种方式声明的函数 foo,就应该以 foo(); 的方式调用。

但是括号则不同,它将一个函数声明转化成了一个表达式,解析器不再以函数声明的方式处理函数a,而是作为一个函数表达式处理,也因此只有在程序执行到函数a时它才能被访问。

所以,任何消除函数声明和函数表达式间歧义的方法,都可以被解析器正确识别。比如:

1
2
3
var i = function(){return 10}();        // undefined  
1 && function(){return true}(); // true
1, function(){alert('ghostwang')}(); // undefined

赋值,逻辑,甚至是逗号,各种操作符都可以告诉解析器,这个不是函数声明,它是个函数表达式。并且,对函数一元运算可以算的上是消除歧义最快的方式,感叹号只是其中之一,如果不在乎返回值,这些一元运算都是有效的:

1
2
3
4
!function(){alert('ghostwang')}()        // true
+function(){alert('ghostwang')}() // NaN
-function(){alert('ghostwang')}() // NaN
~function(){alert('ghostwang')}() // -1

甚至下面这些关键字,都能很好的工作:

1
2
3
void function(){alert('ghostwang')}()        // undefined  
new function(){alert('ghostwang')}() // Object
delete function(){alert('ghostwang')}() // true

最后,括号做的事情也是一样的,消除歧义才是它真正的工作,而不是把函数作为一个整体,所以无论括号括在声明上还是把整个函数都括在里面,都是合法的:

1
2
(function(){alert('ghostwang')})()        // undefined
(function(){alert('ghostwang')}()) // undefined

说了这么多,实则在说的一些都是最为基础的概念——语句,表达式,表达式语句,这些概念如同指针与指针变量一样容易产生混淆。虽然这种混淆对编程无表征影响,但却是一块绊脚石随时可能因为它而头破血流。

最后讨论下性能。不同的方式产生的结果并不相同,而且,差别很大,因浏览器而异。

但我们还是可以从中找出很多共性:new方法永远最慢——这也是理所当然的。其它方面很多差距其实不大,但有一点可以肯定的是,感叹号并非最为理想的选择。反观传统的括号,在测试里表现始终很快,在大多数情况下比感叹号更快——所以平时我们常用的方式毫无问题,甚至可以说是最优的。加减号在chrome表现惊人,而且在其他浏览器下也普遍很快,相比感叹号效果更好。

当然这只是个简单测试,不能说明问题。但有些结论是有意义的:括号和加减号最优

Function构造函数

每个 JavaScript 函数实际上都是一个 Function 对象。运行 (function(){}).constructor === Function // true 便可以得到这个结论。

这样的话,那我们申明函数一定还有new函数对象这种方式:

Function 构造函数创建一个新的 Function 对象,与 eval 不同的是,Function创建的函数只能在全局作用域中运行(注意区分浏览器环境和node环境)。以调用函数的方式调用Function的构造函数(而不是使用new` 关键字) 跟以构造函数来调用是一样的。

上面这句话极其重要,上面这句话极其要,上面这句话极其重要!

语法:

1
new Function ([arg1[, arg2[, ...argN]],] functionBody)

我们先看一个经典的大厂面试题:

1
2
3
4
5
6
7
8
9
10
var a = 1,
b = 2;

function foo() {
var b = 3;
return new Function('c', 'console.log(a + b + c)');
}

var test = foo();
test(4); // 7

为什么结果为7,而不是8呢?那如果我改成下面这种常规写法呢?结果一样吗?

1
2
3
4
5
6
7
8
9
10
11
12
var a = 1,
b = 2;

function foo() {
var b = 3;
return function(c) {
console.log(a + b + c)
}
}

var test = foo();
test(4); // 8

再看下面这道题,结果又是什么?

1
2
3
4
5
6
7
8
9
10
var a = 1,
b = 2;

function foo() {
var b = 3;
return new Function('c', 'var b = 3; console.log(a + b + c)');
}

var test = foo();
test(4); // 8

回头看一下刚开始那句话:**Function 创建的函数只能在全局作用域中运行**。

所以new Function构造的函数,函数体内访问的作用域是全局作用域,他不会创建闭包;一起看看MDN官方的解释:

Function_构造器与函数声明之间的不同:

Function 构造器创建的函数不会创建当前环境的闭包,它们总是被创建于全局环境,因此在运行时它们只能访问全局变量和自己的局部变量,不能访问它们被 Function 构造器创建时所在的作用域的变量。这一点与使用 eval 执行创建函数的代码不同

里面提到的eval是什么?

eval() 函数可计算某个字符串,并执行其中的的JavaScript代码,一般业务开发中用不到这个函数,但是我在一些源码中看到过,babel的源码和vue源码中都有见到过,业务代码中基本没出现过。因为这个东西很容易被滥用,并且对网站有危险性,可想而知这样的函数功能使用不当一定会造成js脚本注入(xss)的安全隐患。

我们用eval来实现同样的代码,看看结果又会是什么?

1
2
3
4
5
6
7
8
9
var a = 1,
b = 2;

function foo() {
var b = 3;
eval('!function _(c){ console.log(a + b + c) }(4)');
}

foo(); // 8

因为eval执行代码作用域指向上方本地作用域,这一点与new Function相反,开头也介绍了这句话。

Node环境异同解析

先了解一下javascrip的顶级作用域:

global是javascript运行时所在宿主环境提供的全局对象

window对象是浏览器的一个web api,可以说是global在浏览器中的具体表现

global对象是单体内置对象,即不依赖宿主环境的对象,而window对象依赖浏览器

node环境执行以下代码看看执行结果是否与浏览器环境下相同:

1
2
3
4
5
6
7
8
9
10
var a = 1,
b = 2;

function foo() {
var b = 3;
return new Function('c', 'console.log(a + b + c)');
}

var test = foo();
test(4);

执行结果如下:

image-20210308090315947

因为在node环境下你在当前文件定义的变量作用域是当前模块的作用域,而不是顶级作用域,node的顶级作用域是globalnew Function中的函数体是全局作用域,当然访问不了你模块中的作用域了。

原型链

原型链相关内容可以参考往期文章: 一题搞懂原型链

1
2
3
4
var test1 = new Function("console.log('test1')");
var test2 = Function("console.log('test2')");
console.log(test1.__proto__ === Function.prototype); // true
console.log(Function.__proto__ === Function.prototype); // true