进击JavaScript核心 --- (2)函数和预解析机制

一、函数

每个函数都是 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的作用域问题,前者属于函数作用域,后者属于全局作用域,所以导致输出结果完全不同

原文地址:https://www.cnblogs.com/rogerwu/p/10923375.html

时间: 2024-10-09 14:30:18

进击JavaScript核心 --- (2)函数和预解析机制的相关文章

死磕JavaScript变量和函数的预解析

预解析:在解析代码之前做一些处理 预解析做什么处理? 把变量的声明提前了----提前到当前所在的作用域的最上面 函数的声明也会被提前---提前到当前所在的作用域的最上面 那么我们现在开始举几个例子 1.观察下方的第一个红框中的代码,猜猜它的结果是什么? 通过运行我们发现,代码竟然神奇的没有报错?但是输出的也不是下面赋值的1而是undefined,这到底是为什么呢?其实这就是因为js引擎的预解析将num这个变量的声明提前到作用域的最上方(num是全局变量所以提前到最外层也就是script标签内的最

轻松搞定javascript预解析机制(搞定后,一切有关变态面试题都是浮云~~)

hey,guys!我们一起总结一下JS预解析吧! 首先,我们得搞清楚JS预解析和JS逐行执行的关系.其实它们两并不冲突,一个例子轻松理解它们的关系: 你去酒店吃饭,吃饭前你得看下菜谱,点下菜(JS预解析),但吃的时候还是一口一口的吃(JS逐行执行)! OK,解决下面五个问题,JS预解析就算过了~~(前提:对JS变量作用域有清晰理解) 一.JS预解析是什么? 其实就是对程序要用到的材料(变量,函数)给一个初始值,并存到一个表中(我自己虚构的),当程序运行到那一行时,就来这个表看有没有初始值,没有就

从var func=function 和 function func()区别谈Javascript的预解析机制

var func=function 和 function func()在意义上没有任何不同,但其解释优先级不同:后者会先于同一语句级的其他语句. 即: { var k = xx(); function xx(){return 5;} } 不会出错,而 { var k = xx(); var xx = function(){return 5;} } 则会出错. 为什么会这样呢?这就要引出javascript中的预解析机制来解释了. JavaScript解析过程分为两个阶段,一个是编译阶段,另外一个

F火狐 不能对 {}括号内的 函数进行预解析

<!DOCTYPE HTML> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> <title>无标题文档</title> <script> // alert(a);// ... alert( fn1 );// F火狐 不能对{}括号内的函数进行预解析 var a = 1

函数_预解析_对象

函数练习: 判断一个数是否是质数 1 <!DOCTYPE html> 2 <html lang="en"> 3 <head> 4 <meta charset="UTF-8"> 5 <title>这是标题</title> 6 <script> 7 function isPrime(x) { 8 for (var i = 2; i * i <= x; i++) { 9 if (x

第06课:作用域、JS预解析机制

从字面上理解----域就是空间.范围.区域,作用就是读.写,所以作用域我们可以简单理解为:在什么样空间或者范围内对数据进行什么样的读或写操作. 看一下代码 alert(a); // 为什么是undefined var a = 1; alert(a); //为什么浏览器无反应 a = 1; 要了解为什么这些现象必须先知道浏览器是如何读取js代码,而这段浏览器专门用来读取js代码的片段我们称之为-----js解析器  首先,我们来理解“JS解析器”是如何工作的? 先看如下代码 alert(a); /

JS——变量和函数的预解析、匿名函数、函数传参、return

JS解析过程分为两个阶段:编译阶段.执行阶段.在编译阶段会将函数function的声明和定义都提前,而将变量var的声明提前,并将var定义的变量赋值为undefined. 匿名函数: window.onload = function () { var oBtn01 = document.getElementById('btn01'); var oBtn02 = document.getElementById('btn02'); //注意这里不能有括号,skin01()的话就立刻执行了哦,所以不

Javascript 的变量提升与预解析

一.什么是变量提升 在ES6之前,JavaScript没有块级作用域(一对花括号{}即为一个块级作用域),只有全局作用域和函数作用域.变量提升即将变量声明提升到它所在作用域的最开始的部分 二.怎么实现变量提升 Js 运行前有一个预编译的过程,预编译完成后,在一步步执行.那么在预编译的过程中,会执行三个动作: 1.分析参数, 2.分析变量声明, 3.分析函数声明. 具体步骤如下: 1.函数在运行的瞬间,生成一个活动对象(Active Object),简称 AO 2.分析参数 函数先接收形参,添加到

进击JavaScript核心 --- (1)基本数据类型

ES5之前提供了 5种基本数据类型 和 1种引用数据类型 基本数据类型:Undefined, Null, String, Number, Boolean 引用数据类型:Object ES6开始引入了一种新的基本数据类型 Symbol,表示独一无二的值 1.typeof 操作符 typeof 是一个操作符而不是函数,因此后面的圆括号可以用,但不是必须的 "undefined" --- 如果这个值未定义(注意:包括未声明和未初始化) "string" --- 如果这个值