进击JavaScript核心 --- 函数和预解析机制
Posted rogerwu
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了进击JavaScript核心 --- 函数和预解析机制相关的知识,希望对你有一定的参考价值。
一、函数
每个函数都是 Function类型的实例,也具有属性和方法。由于函数也是一个对象,因此函数名实际上也是一个指向函数对象的指针,不会与某个函数绑定
1、函数的定义方式
(1)、函数声明
function add(a, b) { return a + b; }
函数声明提升:在执行代码之前,会先读取函数声明,也就是说,可以把函数声明放在调用它的代码之后
fn(); // 1 function fn() {console.log(1)}
(2)、函数表达式
var add = function(a, b) { return a + b; };
函数表达式看起来像是常规的变量赋值,由于其function关键字后面没有指定函数名,因此是一个匿名函数
函数表达式必须先赋值,不具备函数声明提升的特性
fn(); // Uncaught TypeError: fn is not a function var fn = function(){console.log(1)};
由于函数声明提升这一特性,导致在某些情况下会出现意想不到的结果,例如:
var flag = true; if(flag) { function fn() { console.log(‘flag 为true‘) } } else{ function fn() { console.log(‘flag 为false‘) } } fn(); // chrome, firefox, ie11 输出 flag 为true // ie10及以下 输出 flag 为false
本意是想flag为true时输出 ‘flag 为true‘, flag为false时输出 ‘flag 为false‘,为何结果却不尽相同呢?究其原因就在于函数声明提升,执行代码时首先读取函数声明,而 if...else...代码块同属于全局作用域,因此后面的同名函数会覆盖前面的函数,最终函数fn就只剩下一个 function fn(){console.log(‘flag 为false‘)}
由于函数声明提升导致的这一结果令人大为意外,因此,js引擎会尝试修正错误,将其转换为合理状态,但不同浏览器版本的做法并不一致
此时,函数表达式就可以解决这个问题
var flag = true; var fn; if(flag) { fn = function() { console.log(‘flag 为true‘); } } else{ fn = function() { console.log(‘flag 为false‘); } } fn() //chrome, firefox, ie7-11 均输出 flag 为true
其实这个也很好理解,js预解析时,fn和flag均被初始化为undefined,然后代码从上到下逐行执行,首先给flag赋值为true,进入if语句,为fn赋值为 function fn(){console.log(‘flag 为true‘)}
关于函数表达式,还有一种写法,命名函数表达式
var add = function f(a, b) { console.log(a + b); } add(1,2); // 3 f(1,2); // Uncaught ReferenceError: f is not defined var add = function f(a, b) { console.log(f); } console.log(add); add(3, 5); // ƒ f(a, b) { // console.log(f); // }
由此可见,命名函数f也是指向函数的指针,只在函数作用域内部可用
(3)、Function构造函数
var add = new Function(‘a‘, ‘b‘, ‘return a + b‘);
不推荐这种写法,因为这种语句会导致解析两次代码,第一次是解析js代码,第二次解析传入构造函数中的字符串,从而影响性能
2、没有重载
在java中,方法具有重载的特性,即一个类中可以定义有相同名字,但参数不同的多个方法,调用时,会根据不同的参数选择不同的方法
public void add(int a, int b) { System.out.println(a + b); } public void add(int a, int b, int c) { System.out.println(a * b * c); } // 调用时,会根据传入参数的不同,而选择不同的方法,例如传入两个参数,就会调用第一个add方法
而js则没有函数重载的概念
function add(a, b) { console.log(a + b); } function add(a, b, c) { c = c || 2; console.log(a * b * c); } add(1, 2); // 4 (直接调用最后一个同名的函数,并没有重载)
由于函数名可以理解成一个指向函数对象的指针,因此当出现同名函数时,指针就会指向最后一个出现的同名函数,就不存在重载了(如下图所示)
3、调用匿名函数
对于函数声明和函数表达式,调用函数的方式就是在函数名(或变量名)后加一对圆括号
function fn() { console.log(‘hello‘) } fn() // hello
既然fn是一个函数指针,指代函数的代码段,那能否直接在代码段后面加一对圆括号呢?
function fn() { console.log(‘hello‘) }() // Uncaught SyntaxError: Unexpected token ) var fn = function() { console.log(‘hello‘) }() // hello
分别对函数声明和函数表达式执行这一假设,结果出人意料。另外,前面也提到函数声明存在函数声明提升,函数表达式不存在,如果在函数声明前加一个合法的JS标识符呢?
console.log(fn); // ƒ fn() {console.log(‘hello‘);} function fn() { console.log(‘hello‘); } // 在function关键字前面加一个合法的字符,结果就把fn当做一个未定义的变量了 console.log(fn); // Uncaught ReferenceError: fn is not defined +function fn() { console.log(‘hello‘); }
基于此可以大胆猜测,只要是function关键字开头的代码段,js引擎就会将其声明提前,所以函数声明后加一对圆括号会认为是语法错误。结合函数表达式后面直接加圆括号调用函数成功的情况,做出如下尝试:
+function() { console.log(‘hello‘) }() -function() { console.log(‘hello‘) }() *function() { console.log(‘hello‘) }() /function() { console.log(‘hello‘) }() %function() { console.log(‘hello‘) }() // hello // hello // hello // hello // hello
竟然全部成功了,只是这些一元运算符在此处并无实际意义,看起来令人费解。换成空格吧,又会被js引擎给直接跳过,达不到目的,因此可以用括号包裹起来
(function() { console.log(‘hello‘); })(); (function() { console.log(‘hello‘); }()); // hello // hello
无论怎么包,都可以成功调用匿名函数了,我们也不用再困惑调用匿名函数时,圆括号该怎么加了
4、递归调用
递归函数是在一个函数通过名字调用自身的情况下构成的
一个经典的例子就是计算阶乘
// 3! = 3*2*1 // 4! = 4*3*2*1 = 4*3! function factorial(num) { if(num <= 1) { return 1 } return num * factorial(num - 1) } console.log(factorial(5)) // 120 console.log(factorial(4)) // 24
如果现在把函数名factorial换成了jieCheng,执行jieCheng(5) 就会报错了,外面改了,里面也得改,如果是递归的层次较深就比较麻烦。事实上,这样的代码也是不够健壮的
这里有两种解决方案:
(1)、使用 arguments.callee
arguments.callee 是一个指向正在执行的函数的指针,函数名也是指向函数的指针,因此,可以在函数内部用 arguments.callee 来替代函数名
function fn() { console.log(arguments.callee) } fn() // ƒ fn() { // console.log(arguments.callee) // } function factorial(num) { if(num <= 1) { return 1 } return num * arguments.callee(num - 1) } console.log(factorial(5)) // 120
但在严格模式下,不能通过脚本访问 arguments.callee,访问这个属性会导致错误
‘use strict‘ function factorial(num) { if(num <= 1) { return 1 } return num * arguments.callee(num - 1) } console.log(factorial(5)) // Uncaught TypeError: ‘caller‘, ‘callee‘, and ‘arguments‘ properties may not be accessed on strict mode functions or the arguments objects for calls to them
(2)、命名函数表达式
var factorial = function jieCheng(num) { if(num <= 1) { return 1 } return num * jieCheng(num - 1) }; console.log(factorial(5)) // 120 var result = factorial; console.log(result(4)); // 24
5、间接调用
apply()和 call()。这两个方法的用途都是在特定的作用域中调用函数,实际上等于设置函数体内 this 对象的值。
首先,apply()方法接收两个参数:一个是在其中运行函数的作用域,另一个是参数数组。其中,第二个参数可以是 Array 的实例,也可以是arguments 对象。
function add(a, b) { console.log(a + b); } function sum1(a, b) { add.apply(window, [a, b]); } function sum2(a, b) { add.apply(this, arguments) } sum1(1, 2); // 3 sum2(3, 5); // 8
call()方法与 apply()方法的作用相同,它们的区别仅在于接收参数的方式不同。对于 call()方法而言,第一个参数是 this 值没有变化,变化的是其余参数都直接传递给函数。换句话说,在使用call()方法时,传递给函数的参数必须逐个列举出来
var color = ‘red‘; var obj = { color: ‘blue‘ }; function getColor() { console.log(this.color) } getColor.call(this) // red getColor.call(obj) // blue
二、预解析机制
第一步:js运行时,会找所有的var和function关键字
--、把所有var关键字声明的变量提升到各自作用域的顶部并赋初始值为undefined,简单说就是 “声明提前,赋值留在原地”
--、函数声明提升
第二步:从上至下逐行解析代码
var color = ‘red‘; var size = 31; function fn() { console.log(color); var color = ‘blue‘; var size = 29; } fn(); // undefined
// 第一步:在全局作用域内查找所有使用var和function关键字声明的变量,把 color、size、fn 提升到全局作用域顶端并为其赋初始值;同理,在fn函数作用域内执行此操作
// 第二步:从上至下依次执行代码,调用fn函数时,按序执行代码,函数作用域内的输出语句中color此时仅赋初始值undefined
注意:
(1)、如果函数是通过 “函数声明” 的方式定义的,遇到与函数名相同的变量时,不论函数与变量的位置顺序如何,预解析时函数声明会覆盖掉var声明的变量
console.log(fn) // ƒ fn() {} function fn() {} var fn = 32
(2)、如果函数是通过 “函数表达式” 的方式定义的,遇到与函数名相同的变量时,会视同两个var声明的变量,后者会覆盖前者
console.log(fn); // undefined var fn = function() {}; var fn = 32; console.log(fn) // 32
(3)、两个通过 “函数声明” 的方式定义的同名函数,后者会覆盖前者
console.log(fn); // ƒ fn() {console.log(‘你好 世界‘)} function fn() {console.log(‘hello world‘)} function fn() {console.log(‘你好 世界‘)}
预解析练习一:
var fn = 32 function fn() { alert(‘eeee‘) } console.log(fn) // 32 fn() // Uncaught TypeError: fn is not a function console.log(typeof fn) // number // 按照上面的预解析规则,预解析第一步时,fn会被赋值为 function fn() {alert(‘eeee‘)};第二步从上到下逐步执行时,由于函数fn声明提前,优于var声明的fn执行了, // 所以fn会被覆盖为一个Number类型的基本数据类型变量,而不是一个函数,其值为32
预解析练习二:
console.log(a); // function a() {console.log(4);} var a = 1; console.log(a); // 1 function a() { console.log(2); } console.log(a); // 1 var a = 3; console.log(a); // 3 function a() { console.log(4); } console.log(a); // 3 a(); // 报错:不是一个函数
预解析步骤:
(1)、找出当前相同作用域下所有使用var和function关键字声明的变量,由于所有变量都是同名变量,按照规则,权值最高的是最后一个声明的同名的function,所以第一行输出 function a() {console.log(4);}
(2)、从上至下逐步执行代码,在第二行为变量a 赋值为1,因此输出了一个1
(3)、执行到第一个函数a,由于没有调用,直接跳过不会输出里面的2,执行到下一行输出1
(4)、继续执行,为a重新赋值为3,因此输出了一个3
(5)、执行到第二个函数a,还是没有调用,直接跳过不会输出里面的4,执行到下一行输出3
(6)、最后一行调用函数a,但由于预解析时率先把a赋值为一个函数代码段,后面依次为a赋值为1和3,因此,a是一个Number类型的基本变量,而不是一个函数了
预解析练习三:
var a = 1; function fn(a) { console.log(a); // 999 a = 2; console.log(a) // 2 } fn(999); console.log(a); // 1
预解析步骤:
(1)、全局作用域内,为a赋值为undefined,把函数fn提升到最前面;fn函数作用域内,函数参数在预解析时也视同局部变量,为其赋初始值 undefined
(2)、执行fn函数,传入实参999,为局部变量a赋值为999并输出;重新为a赋值为2,输出2
(3)、由于全局作用域下的a被赋值为1,而函数作用域内部的a是访问不到的,因此直接输出1
预解析练习四:
var a = 1; function fn() { console.log(a); var a = 2; } fn(); // undefined console.log(a); // 1
var a = 1; function fn() { console.log(a); a = 2; } fn(); // 1 console.log(a); // 2
对比两段代码,唯一的区别就是fn函数内的变量a的作用域问题,前者属于函数作用域,后者属于全局作用域,所以导致输出结果完全不同
以上是关于进击JavaScript核心 --- 函数和预解析机制的主要内容,如果未能解决你的问题,请参考以下文章