参数默认值、递归、预编译、暗示全局变量

368 阅读9分钟

1. 函数初始化参数

1.1 函数参数默认值,默认值:undefined

实际参数未给形式参数传递参数,则形式参数的默认值是undefined;

function fn(a = 1, b) {
	console.log(a, b); 
}
fn(1); // 1 undefined

1.2 函数默认值的取值条件

函数的实际参数与形式参数在函数体内部具有映射的关系,形参和实参有一方是undefined,那么就取另一方的值作为默认值;

1. 如果形式参数是undefined,则获取实际参数的值

function fn(a, b) {
	console.log(a, b); // 1 undefined
}
fn(1);


function test(a = undefined, b) {
	console.log(a, b);
}
test(1, 2);

2. 如果实际参数是undefined,则获取形式参数的值(形式参数默认值)

function fn(a = 1, b) {
	console.log(a, b); // 1 2
}
fn(undefined, 2);


function fn(a = 1, b) {
	console.log(a, b); // 1 1
}
fn(undefined, 1);

3. 如果实际参数与形式参数都是undefined,则值就是undefined

function fn(a, b) {
  console.log(a, b); // undefined, undefined
}
fn();

4. 特例:如果实际参数的数量 < 小于形式参数数量

实际参数的数量 < 小于形式参数数量,此时函数内部,实际参数与形式参数的映射关系由于参数数量的问题,无法产生映射关系,此时改变形式参数的值,实际参数不受影响;只有实际参数与形式参数一一对应时,才会产生映射的关系;

function fn(a, b) {
	a = 3; // 和fn(a = 3, b = 4)没区别
	b = 4;
	console.log(a, b); // 3 4
  console.log(arguments[0],arguments[1]); // undefined undefined
}
fn();

1.3 低版本函数默认值的写法

  1. 逻辑运算符 || 或运算实现,此种写法存在弊端,如果实际参数Boolean(实参)返回false的话,那么就会取||后面的数;例如实际参数传0,此时形式参数a的值是1,但是实际上应该是0;
// 逻辑运算
function fn(a, b) {
	var a = arguments[0] || 1;
  var b = arguments[1] || 2;
  console.log(a, b); // 1 2
}
fn();
  1. 三元运算符实现,可以避免上述的问题;
// 三元运算符
function fn(a, b) {
	var a = typeof(arguments[0]) === 'undefined'? 2 : arguments[0];
  var b = typeof(arguments[1]) === 'undefined'? 2 : arguments[1];
  console.log(a, b);
}
fn();

2. 递归

2.1 递归的两个要素

  1. 递归的规律
  2. 递归的出口,递归的return值一直在等待着计算结果,然后逐步返回上层的输出结果

2.2 递归例题

1. 输出N的阶乘

var n = parseInt(window.prompt('N=?'));
function factorial(n) {
  // 错误机制
  if(n <= 0) {
  	return '输入有误';
  }
	// 递归出口
  if(n === 1) {
  	return 1;
  }
  return n * factorial(n - 1);
}
var num = factorial(n);
console.log(num);

// 执行过程形成的队列
factorial(5) = factorial(4) * 5;
factorial(4) = factorial(3) * 4;
factorial(3) = factorial(2) * 3;
factorial(2) = factorial(1) * 2;
factorial(1) = 1;

2. 输出斐波那契数列的N位

var n = parseInt(window.prompt('N=?'));
function fobo(n) {
  // 错误机制
  if(n < 1) {
  	return '输入有误';
  }
	if(n === 1 || n === 2) {
  	return 1;
  }
  return fobo(n - 1) + fobo(n - 2);
}
var num = fobo(n);
console.log(num);

// 执行过程中的形成的队列
fobo(5) = fobo(4) + fobo(3);
fobo(4) = fobo(3) + fobo(2);
fobo(3) = fobo(2) + fobo(1);
fobo(2) = 1;
fobo(1) = 1;

3. 预编译(重点)

3.1 JavaScript引擎解析代码的执行顺序

  1. 检查通篇的代码语法错误,如果有则直接抛出语法错误(任何代码不执行),没有则进行预编译;
  2. 进行预编译;
  3. 解释一行,执行一行;
  4. 函数声明整体提升,变量只有声明提升,赋值不提升(所以存在先声明变量,后赋值 )

3.2 暗示全局变量(imply global variable)

没有用var声明的变量是全局变量,在全局环境下,变量和方法都是挂载到window对象上,相当于window对象的属性和方法;

  1. 为什么window.c一个不存在的属性是undefined,而直接打印console.log(c)一个不存在的属性报错(Uncaught ReferenceError: c is not defined)呢?因为window.c是属于对象语法的性质,在通过window.c形式调用不存在的属性返回的就是undefined;
var a = 3;
var b = 4;
--->
window = {
	a : 3,
	b : 4
}
  1. 在全局声明变量和声明函数,都是作为window对象的属性;但是二者并不是同一个东西,window中的属性与声明的变量存在映射的关系,所以二者能够同时随之改变;二者在内存中存放的位置都不同,变量存放在栈内存中,window对象存放在堆内存中;
var a = 1,
		b = 2;
console.log(window.b);

window = {
	a:1,
	b:2
}

3.3 AO(activation object)活跃对象,函数执行期上下文

1. 在AO中JavaScript引擎预编译的步骤

  1. 寻找函数的形参和变量声明
  2. 实参的值赋值给形参
  3. 寻找函数声明
  4. 执行函数

2. AO例题:

function test(a, b) {
	console.log(a); // 1
  c = 0;
  var c;
  a = 5;
  b = 6;
  console.log(b); // 6
  function b(){};
  function d(){};
  console.log(b); // 6
}
test(1);

// 预编译
// AO = {
	 a:undefined,
     ---> 1
		 ---> 5
   b:undefined,
     ---> function b(){}
		 ---> 6
   c:undefined,
     ---> 0
   d:function d(){}
}

3.4 GO(global object)全局对象,全局执行期上下文

1. 在GO中JavaScript引擎预编译的步骤

  1. 寻找变量声明
  2. 寻找函数声明
  3. 执行代码

2. GO全局对象与window对象相等 GO===window

console.log(a, b); // function a(){}  undefined
function a(){};
var b = function(){};

// 预编译
GO = {
	b:undefind,
  --> function(){}
  a:function a(){},
}

3.5 关于AO与GO的一些疑惑点

  1. 函数声明在预编译期间进行声明提升,为什么函数表达式不会呢?

函数声明在预编译期间进行函数声明提升,而函数表达式在预编译期间,先进行变量fn的声明提升,但是对于后面赋值的部分并不会进行提升,相当于var fn = 3这种形式,所以函数表达式并不会进行函数声明提升;

因为在GO的预编译期间,首先寻找变量声明,var fn就是变量声明,而var fn = function(){};虽然是函数声明的一种方式,但是它的执行顺序是,先创建一个变量fn,然后通过赋值的方式将function fn(){}函数在栈内存中的地址赋值给变量fn;所以function(){}函数是在JS引擎执行脚本语言代码的时候定义的。所以在预编译第二阶段寻找函数声明的时候找不到这个匿名函数。这个匿名函数是在JS引擎执行脚本语言是被定义的,GO是对象不会执行。

console.log(fn); // undefined
var fn = function(){};
  1. 对象内部的函数会函数声明提升吗?

对象内部匿名函数显然不会提升,而且这两个匿名函数是在GO执行(JS引擎执行脚本语言)的时候定义的,和上面的是同一道理。

var obj = {
	name:function(){},
  sex:function(){},
  age:18
}
  1. 预编译的存在的意义

GO和AO的定义是让JS引擎看的,预编译是为了让JS引擎能够更好的去解析代码。上面的例子,在JS引擎执行脚本语言的时候,才将{}赋值给obj变量,obj变量内部存放是的指向{}的指针,obj的name属性存放着指向function(){}的指针,当这个对象被定义的时候,匿名函数才会被定义。反过来说,如果单纯的提升一个匿名函数,这个匿名函数也没有指向,对于代码整体来说没有任何意义。预编译去寻找变量的目的是:这次执行先要确定有哪些变量,然后才好去赋值。

4. AO与GO预编译的综合练习题

var b = 1;
function test() {
	var a = 1;
  var b = 2;
  console.log(b); // 2
}
test();

// 预解析
GO = {
	b: undefined,
  --> 1
  test:function test(){},
 
}

AO = {
	a:undefined,
  --> 1,
  b:undefined,
  --> 2,
}
var b = 3;
console.log(a); // function a(a) {}
function a(a) {
	console.log(a); // function a() {}
  var a = 2;
  console.log(a); // 2
  function a() {};
 	var b = 5;
  console.log(b); // 5
}
a(1);

// GO = {
	 b:undefined,
     --> 3,
   a:function a(a) {}
}

// AO = {
	 a:undefined,
     --> 1
     --> function a(){}
     --> 2
   b:undefined,
     --> 5
}

1. 因为AO环境中预解析时有a变量,所以就不去GO找了

a = 1;
function test() {
  // 因为AO环境中预解析时有a变量,所以就不去GO找了
	console.log(a); // undefined
  a = 2;
  console.log(a); // 2
  var a = 3;
  console.log(a); // 3
}
test();
var a;

// GO = {
		a:undefined,
      --> 1
  	test:function() {},
}

// 因为AO环境中预解析时有a变量,所以就不去GO找了
// AO = {
		a:undefined,
    --> 2
    --> 3
}

2. 预编译不看语句判断,只看有没有变量声明和函数声明

预编译不看语句判断,所以if语句中的变量b能够变量声明提升到test函数的AO环境执行期上下文中;

// 预编译不看语句判断,只看有没有变量声明和函数声明
function test() {
	console.log(b); // undefined
  if(a) {
  	var b = 2;
  }
  c = 3;
  console.log(c); // 3
}
var a;
test();
a = 1;
console.log(a); // 1

// GO = {
	 a:undefined,
     --> 1
   test:function test(){},
   c:undefined,
     --> 3
}

// AO = {
	 b:undefined,
   	--> 2
}

作业:

function test() {
  return a;
  a = 1;
  function a() {}
  var a = 2;
}
console.log(test()); // function a() {}

// AO = {
	 a:undefined,
    --> function a() {}
    // 执行
    --> return function a(){}
}

1. 预编译只管声明,不管赋值的部分,赋值是在执行期进行的

  1. 注意此时test函数中第一行的变量a是局部变量,虽然此时没有var声明,看似是全局变量,其实不然;因为在test函数预编译的阶段,变量a已经被声明,而后面给变量a赋值的时候是在函数的执行期执行的,所以a=1赋值的时候,局部的作用域已经存在变量a了;
function test() {
  a = 1; // 局部变量;
  function a() {}
  var a = 2;
  return a;
}
console.log(test()); // 2

//AO = {
a:undefined,
  --> function a(){}
  --> 1
  --> 2
  --> return a
}

2. 预编译阶段,关于全局变量的情况;

为什么下面的例子,一个输出undefined,一个抛出异常呢?

  1. 例1:首先在全局的预编译阶段已经确立全局环境中存在变量f,函数fn在执行期间在fn环境中未发现变量f,则向外层作用域中寻找变量f,但是此时变量未被赋值,所以是undefined;
  2. 例2:全局的预编译阶段并未声明变量f,虽然f = 5,暗示变量f是全局变量,但是由于变量f未被var声明,所以函数预编译阶段并不会提升,f = 5只会在函数执行的期间,被默认为是全局变量,所以函数内部未声明变量f,全局未声明f,代码抛出异常;
// 例1:
var f;
function fn() {
  console.log(f); // undefined
  f = 5;
}
fn();

// 例2:
function fn() {
  console.log(f); // Uncaught RefferenceError: f is not defined;
  f = 5;
}
fn();

// 例3:
function fn() {
  console.log(f); // undefined
  f = 5;
	var f; // 局部变量;
}
fn();
a = 1;
function test(e) {
	function e() {}
  arguments[0] = 2;
  console.log(e); // 2
  if(a) {
  	var b = 3;
  }
  var c;
  a = 4;
  var a;
  console.log(b);  // undefined
  f = 5;
  console.log(c); // undefined
  console.log(a); // 4
}
var a;
test(1);
console.log(a); // 1
console.log(f); // 5

// GO = {
	 a:undefined,
     --> 1
   test:function test(e) {}
	 f:undefined,
     --> 5
}

// AO = {
	 e:undefined,
     --> 1
     --> function e() {}
     --> 2
   a:undefined,
     --> 4
   b:undefined,
     --> undefined
   c:undefined,
}