你不知道的『Function』

蝉姐
• 阅读 991

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

函数申明与函数表达式

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

你不知道的『Function』

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

(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` 关键字) 跟以构造函数来调用是一样的。

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

语法:

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

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

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呢?那如果我改成下面这种常规写法呢?结果一样吗?

var a = 1,
    b = 2;

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

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

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

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来实现同样的代码,看看结果又会是什么?

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环境执行以下代码看看执行结果是否与浏览器环境下相同:

var a = 1,
    b = 2;

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

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

执行结果如下:

你不知道的『Function』

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

原型链

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

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
点赞
收藏
评论区
推荐文章
九路 九路
4年前
Go 函数是“一等公民”的理解
函数(function)作为现代编程语言的基本语法元素存在于支持各种范式(paradigm)的主流编程语言当中。无论是命令式语言C、多范式通用编程语言C,还是面向对象编程语言Java、Ruby,亦或是函数式语言Haskell、动态脚本语言Python、PHP、JavaScript,函数这一语法元素都是当仁不让的核心。Go语言以“成为新一代系统
徐小夕 徐小夕
4年前
如何用不到200行代码写一款属于自己的js类库
前言JavaScript的核心是支持面向对象的,同时它也提供了强大灵活的OOP语言能力。本文将使用面向对象的方式,来教大家用原生js写出一个类似jQuery这样的类库。我们将会学到如下知识点:闭包:减少变量污染,缩短变量查找范围自执行函数在对象中的运用extend的实现原理如何实现跨浏览器的事件监听原型链与继承接下来我会对类库
待兔 待兔
1年前
手写Java HashMap源码
HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22
带我的粉丝们一起揭秘spring aop底层原理及实现
实在是不知道写什么了,博主变low了呀。springaop使得我们的aop开发工作变得简单,这是众所周知的今天还是带我的粉丝们一起揭秘springaop底层原理及实现吧哈哈哈哈AOP面向切面编程:主要是通过切面类来提高代码的复用,降低业务代码的耦合性,从而提高开发效率。主要的功能是:日志记录,性能统计,安全控制,事务处理,异常处理等等。AOP实现原理
Karen110 Karen110
3年前
​一篇文章总结一下Python库中关于时间的常见操作
前言本次来总结一下关于Python时间的相关操作,有一个有趣的问题。如果你的业务用不到时间相关的操作,你的业务基本上会一直用不到。但是如果你的业务一旦用到了时间操作,你就会发现,淦,到处都是时间操作。。。所以思来想去,还是总结一下吧,本次会采用类型注解方式。time包importtime时间戳从1970年1月1日00:00:00标准时区诞生到现在
Wesley13 Wesley13
3年前
Java:类与继承
  对于面向对象的程序设计语言来说,类毫无疑问是其最重要的基础。抽象、封装、继承、多态这四大特性都离不开类,只有存在类,才能体现面向对象编程的特点,今天我们就来了解一些类与继承的相关知识。首先,我们讲述一下与类的初始化相关的东西,然后再从几个方面阐述继承这一大特性。以下是本文的目录大纲:  一.你了解类吗?  二.你了解继承吗?  三.常见的面试
Wesley13 Wesley13
3年前
JS必知的6种继承方式
JS作为面向对象的弱类型语言,继承也是其非常强大的特性之一。那么如何在JS中实现继承呢?让我们拭目以待JS继承的实现方式既然要实现继承,那么首先我们得有一个父类,代码如下:// 父类function Person(name) { // 给构造函数添加了参数  this.name  name;
Stella981 Stella981
3年前
Javascript 是如何体现继承的 ?
js继承的概念js里常用的如下两种继承方式:原型链继承(对象间的继承)类式继承(构造函数间的继承) 由于js不像java那样是真正面向对象的语言,js是基于对象的,它没有类的概念。所以,要想实现继承,可以用js的原型prototype机制或者用apply和call方法去实现在面向对象的语言中,我们使用类来创建一个自定义对象
Stella981 Stella981
3年前
35个你也许不知道的Google开源项目
Google是支持开源运动的最大公司之一,它们现在总共发布有超过500个的开源项目(大部分都是利用它们的API来完成),本文将列举一些有趣的开源项目,其中很可能有不少你不知道的哦。文本文件处理:GoogleCRUSH(CustomReportingUtilitiesforSHell)CRUSH是为命令行
Stella981 Stella981
3年前
JavaScript面向对象编程的15种设计模式
在程序设计中有很多实用的设计模式,而其中大部分语言的实现都是基于“类”。在JavaScript中并没有类这种概念,面向对象编程不是基于类,而是基于原型去面向对象编程,JS中的函数属于一等对象,而基于JS中闭包与弱类型等特性,在实现一些设计模式的方式上与众不同。ps:本文之讲述面向对象编程的设计模式策略,JavaScript原型的基础请参考阮一峰面向
浅谈服务接口的高可用设计
作为一个后端研发人员,开发服务接口是我正常不过的工作了,这些接口不管是面向前端HTTP或者是供其他服务RPC远程调用的,都绕不开一个共同的话题就是“高可用”,接口开发往往看似简单,但保证高可用这块实现起来却不并没有想想的那么容易,接下来我们就看一下,一个高可用的接口是该考虑哪些内容,同时文中有不足的欢迎批评指正。
蝉姐
蝉姐
Lv1
载着我满满的怀念,你渐行渐远。
文章
2
粉丝
0
获赞
0