今天在写代码的时候,我犯了一个很low的错误,废话不多说,直接上代码:
1 function () { 2 console.log(‘hello world‘); 3 }()
大家看到之后,第一反应肯定会认为是个语法错误,可是自己仔细想想,这是什么原因?似乎还不能解释清楚,好奇宝宝模式立即启动,经过查阅相关资料得到了答案,接下来我们一起来探讨下其中的原理。
疑惑解答
大家有没有考虑过为什么上面这种写法会报错?
原来,浏览器遇到function关键字的时候会认为这是一个函数声明,函数声明必须包括:关键字function、函数名、形参、函数体。在解析上面代码的时候,解析器发现没有出现函数名而直接出现了(),浏览器便会认为这种定义不符合规范,所以就报错了。
既然是缺少函数名,如果我们给它添加函数名,是不是会正确调用?
1 function hello () { 2 console.log(‘hello world‘); 3 }()
让我们静静等待奇迹出现!
哎,浏览器在解析的时候怎么又报错了?
其实是函数声明的缘故,也就是说通过声明的函数会被提升到其他代码的前面。提升之后应该是这样了:
1 function hello () { 2 console.log(‘hello world‘); 3 } 4 ...// do something 5 (); // 咦?这是什么东东?
解析器对此也很茫然,不知道该按照什么标准去解析,只能告诉你写的不够规范了。 大家看一下下面的这种用法,会不会感觉很熟悉?
1 var hello = function () { 2 console.log(‘hello world‘); 3 } 4 hello();
试想一下,如果用()把上面的函数名hello包裹起来,会发生什么
1 var hello = function () { 2 console.log(‘hello world‘); 3 } 4 (hello)();
Bingo,浏览器输出了“hello world”,这种写法是不是特别像数学中的结合律。
继续对上面的函数调用做一些改变,如果把函数名hello替换成匿名函数,猜想应该也可以调用成功。
1 (function () { 2 console.log(‘hello world‘); 3 })();
果然达到了预期的结果,调用成功了!这是为什么?
因为用()把匿名函数包裹起来,解析器便会认为这是一个函数表达式,函数表达式的后面添加()显然是可以正确执行的。此外,除了()之外,也可以使用!等常见的一元运算符来执行匿名函数。
1 !function() { 2 console.log(‘hello world‘); 3 }();
上面这段代码会输出“hello world”。
Tips:
说到函数定义,需要注意函数声明和函数表达式两者的区别(大神可以跳过喔~):前者有个函数声明提升的过程,在代码预解析的时候,会把函数声明提升到代码的顶部,所以在声明函数位置之前调用函数并不会出错;而后者只是进行变量提升,因此,解析器只有执行函数表达式的代码之后才可以调用该函数。
说到这里,想起了一个老生常谈的问题,函数总是在特定的作用域中执行,函数中this的指向是不是也把好多同学弄的一知半解呢?让我们来继续研究一下~~~
走进函数的this
一般情况下,哪个对象调用函数(方法),则this便指向哪个对象。
通过一个例子来说明该如何确定this的指向。
1 var obj= { 2 number: 1, 3 getOwnNumber: function () { 4 var number = 2; 5 return this.number; 6 }, 7 getNumber: function () { 8 var number = 3; 9 return function () { 10 var number = 4; 11 return this.number; 12 }; 13 } 14 } 15 console.log(obj.getOwnNumber()); 16 console.log(obj.getNumber()());
大家猜一下,第一个输出结果是多少?大家不要犹豫,答案是1,就这么简单!
很明显,是obj调用的getOwnNumber方法,而obj对象内部定义的number值为1,所以第一个输出结果理所应当是1。
问题来了,第二个输出结果是多少?1?2?3?4?
还是同样的方法,我们找一找this到底指向哪个对象。obj.getNumber()运行结果是一个匿名函数,然后再执行匿名函数。我们可以把这个过程拆分一下,如下:
1 var fun = obj.getNumber() // 返回一个匿名函数 2 fun();
经过分解之后,很明显,函数是在全局作用域中调用的,所以此处的this理应指向window对象,window对象中没有定义number,所以结果是undefined。
假想一下,当代码复杂之后,确定this的指向是不是更加麻烦?别急,ES6规范提供了箭头函数的语法,可以帮助我们解决这个问题。箭头函数是何方神圣,该怎么定义呢?箭头函数的基本写法如下:
1 const hello = () => { 2 console.log(‘hello‘); 3 }
我们尝试使用箭头函数来改造obj对象的getNumber方法:
1 const obj= { 2 number: 1, 3 getOwnNumber() { 4 const number = 2; 5 return this.number; 6 }, 7 getNumber() { 8 const number = 3; 9 return () => { 10 const number = 4; 11 return this.number; 12 }; 13 } 14 } 15 console.log(obj.getOwnNumber()); 16 console.log(obj.getNumber()());
运行之后会发现两个输出结果都是1。这是为什么?
因为箭头函数内部的this是指向定义函数时所在的对象,在上面的代码中,this指向了obj对象,所以输出1。有了箭头函数之后,我们不再需要通过变量保存对象的this指针或者通过bind方法改变this的指针,轻松实现我们预期的效果。
箭头函数和普通函数的主要区别:(摘抄自阮一峰大神的《ES6标准入门》):
- 箭头函数体内的this就是定义时所在的对象,而不是使用时所在的对象。
- 箭头函数不可以当做构造函数。也就是说,不可以使用new命令,否则会抛出一个错误。
- 不可以使用arguments对象,该对象在函数体内不存在。可以使用rest参数代替。
- 不可以使用yield命令,因此箭头函数不能用作Generator函数
但是箭头函数也不是可以在任何场景下都能使用,如果我们要改变this的指向该怎么办?在ES6出现之前,我们可以使用call、apply和bind方法来改变函数中this的指向,ES7提出了使用双冒号(::)的运算符,使得我们可以通过这个运算符来改变箭头函数中this的指向。
除了箭头函数之外,ES6又对之前的函数做了哪些优化?
构造函数
在其他语言中可以通过类实现面向对象的功能,但是在ES6规范公布之前,JavaScript中并没有类的概念,面向对象的语法只能通过构造函数的方式来实现。
1 function Zhuanzhuan (name) { 2 this.name = name; 3 } 4 Zhuanzhuan.prototype.say = function () { 5 console.log(‘hi~I am zhuanzhuan‘); 6 }
要想实例化这个“类”,必须使用new关键字
1 var zhuanzhuan = new Zhuanzhuan(‘zhuanzhuan‘);
在ES6之前,使用构造函数来创建对象,阅读起来并不是很清晰。ES6提供的class语法跟其他面向对象的语言语法较为接近。 使用ES6的语法来改写一下上面用构造函数定义的“类”。
1 class Zhuanzhuan { 2 constructor (name) { 3 this.name = name; 4 } 5 say () { 6 console.log(‘hi~I am zhuanzhuan‘); 7 } 8 } 9 let zhuanzhuan = new Zhuanzhuan(‘zhuanzhuan‘);
看起来是不是特别像C++语言中的面向对象风格呢,出现class之后,妈妈再也不用担心我写的类不够清晰了~~~
JavaScript语言中的函数内容相当丰富,需要我们不断去通过实践去加深理解。骐骥一跃,不能十步,驽马十驾,功在不舍。多总结,多思考。大家如果有好的想法,可以相互交流和分享,每天进步一点点,不断提高自己的专业技能。
这就是我因为一个小错误,引发的思考。
如果你喜欢我们的文章,关注我们的公众号和我们互动吧。