JS-口袋书第-3-章JavaScript-函数

38次阅读

共计 8239 个字符,预计需要花费 21 分钟才能阅读完成。

阿里云最近在做活动,低至 2 折,有兴趣可以看看:
https://promotion.aliyun.com/…


函数是什么

函数是完成某个特定功能的一组语句。如没有函数,完成任务可能需要五行、十行、甚至更多的代码。这时我们就可以把完成特定功能的代码块放到一个函数里,直接调用这个函数,就省重复输入大量代码的麻烦。

函数可以概括为:一次封装,四处使用。

函数的定义

函数的定义方式通常有三种:函数声明方式、函数表达式、使用 Function 构造函数。

函数声明方式

语法:

function 函数名(参数 1,参数 2,...){// 要执行的语句}  

例:

// 声明
function sum(num1, num2) {return num1 + num2;}

// 调用
sum(1, 2)  // 3

函数表达式

语法:

var fn = function(参数 1,参数 2,...){// 要执行的语句};

例:

// 声明
var sum = function(num1,num2){return num1+num2;};
// 调用
sum(1, 2) // 3

使用 Function 构造函数

Function 构造函数可以接收任意数量的参数,最后一个参数为函数体,其他的参数则枚举出新函数的参数。其语法为:

new Function("参数 1","参数 2",...,"参数 n","函数体"); 

例:

// 声明
var sum = new Function("num1","num2","return num1+num2");  
// 调用
sum(1, 2) // 3

三种定义方式的区别

三种方式的区别,可以从作用域、效率以及加载顺序来区分。

从作用域上来说 ,函数声明式和函数表达式使用的是局部变量,而 Function() 构造函数却是全局变量,如下所示:

var name = '我是全局变量 name';

// 声明式
function a () {
  var name = '我是函数 a 中的 name';
  return name;
}
console.log(a()); // 打印: "我是函数 a 中的 name"

// 表达式
var b = function() {
  var name = '我是函数 b 中的 name';
  return name; // 打印: "我是函数 b 中的 name"
}
console.log(b())

// Function 构造函数
function c() {
  var name = '我是函数 c 中的 name';
  return new Function('return name')
}
console.log(c()()) // 打印:"我是全局变量 name",因为 Function()返回的是全局变量 name,而不是函数体内的局部变量。

从执行效率上来说 Function() 构造函数的效率要低于其它两种方式,尤其是在循环体中,因为构造函数每执行一次都要重新编译,并且生成新的函数对象。

来个例子:

var start = new Date().getTime()

for(var i = 0; i < 10000000; i++) {var fn = new Function('a', 'b', 'return a + b')
  fn(i, i+1)
}
var end = new Date().getTime();
console.log(` 使用 Function 构造函数方式所需要的时间为:${(end - start)/1000}s`)
// 使用 Function 构造函数方式所需要的时间为:8.646s

start = new Date().getTime();
var fn = function(a, b) {return a + b;}
for(var i = 0; i < 10000000; i++) {fn(i, i+1)
}
end = new Date().getTime();
console.log(` 使用表达式的时间为:${(end - start)/1000}s`)
// 使用表达式的时间为:0.012s

由此可见,在循环体中,使用表达式的执行效率比使用 Function()构造函数快了很多很多。所以在 Web 开发中,为了加快网页加载速度,提高用户体验,我们不建议选择 Function ()构造函数方式来定义函数。

最后是加载顺序,function 方式 (即函数声明式) 是在 JavaScript 编译的时候就加载到作用域中, 而其他两种方式则是在代码执行的时候加载,如果在定义之前调用它,则会返回 undefined

console.log(typeof f) // function
console.log(typeof c) // undefined
console.log(typeof d) // undefined

function f () {return 'JS 深入浅出'}
var c = function () {return 'JS 深入浅出'}
console.log(typeof c) // function
var d = new Function('return"JS 深入浅出 "')
console.log(typeof d) // function

函数的参数和返回值

函数的参数 -arguments

JavaScript 中的函数定义并未指定函数形参的类型,函数调用也未对传入的实参值做任何类型检查。实际上,JavaScript 函数调用甚至不检查传入形参的个数。

function sum(a) {return a + 1;}
console.log(sum(1)); // 2
console.log(sum('1')); // 11
console.log(add()); // NaN
console.log(add(1, 2)); // 2

当实参比形参个数要多时,剩下的实参没有办法直接获得,需要使用即将提到的 arguments 对象。

JavaScript 中的参数在内部用一个数组表示。函数接收到的始终都是这个数组,而不关心数组中包含哪些参数。在函数体内可以通过 arguments 对象来访问这个参数数组,从而获取传递给函数的每一个参数。arguments对象并不是 Array 的实例,它是一个类数组对象,可以使用方括号语法访问它的每一个元素。

function sum (x) {console.log(arguments[0], arguments[1], arguments[2]); // 1 2 3
}

sum(1, 2, 3)

arguments对象的 length 属性显示实参的个数,函数的 length 属性显示形参的个数。

function sum(x, y) {console.log(arguments.length); // 3
  return x + 1;
}
sum(1, 2, 3)
console.log(sum.length) // 2

函数的参数 -arguments

JavaScript 中的函数定义并未指定函数形参的类型,函数调用也未对传入的实参值做任何类型检查。实际上,JavaScript 函数调用甚至不检查传入形参的个数。

function sum(a) {return a + 1;}
console.log(sum(1)); // 2
console.log(sum('1')); // 11
console.log(add()); // NaN
console.log(add(1, 2)); // 2

函数的参数 - 同名参数

在非严格模式下,函数中可以出现同名形参,且只能访问最后出现的该名称的形参。

function sum(x, x, x) {return x;}
console.log(sum(1, 2, 3)) // 3

而在严格模式下,出现同名形参会抛出语法错误。

function sum(x, x, x) {
  'use strict';
  return x;
}
console.log(sum(1, 2, 3)) // SyntaxError: Duplicate parameter name not allowed in this context

函数的参数 - 参数个数

当实参比函数声明指定的形参个数要少,剩下的形参都将设置为 undefined 值。

function sum(x, y) {console.log(x, y);
}
sum(1); // 1 undefined

函数的返回值

所有函数都有返回值,没有 return 语句时,默认返回内容为undefined

function sum1 (x, y) {var total = x + y} 
console.log(sum1()) // undefined

function sum2 (x, y) {return x + y}
console.log(sum2(1, 2)) // 3

如果函数调用时在前面加上了 new 前缀,且返回值不是一个对象,则返回this(该新对象)。

function Book () {this.bookName = 'JS 深入浅出'}

var book = new Book();
console.log(book); // Book {bookName: 'JS 深入浅出'}
console.log(book.constructor); // [Function: Book]

如果返回值是一个对象,则返回该对象。

function Book () {return {bookName: JS 深入浅出}
}

var book = new Book();
console.log(book); // {bookName: 'JS 深入浅出'}
console.log(book.constructor); // [Function: Book]

函数的调用方式

JS 一共有 4 种调用模式:函数调用、方法调用、构造器调用和间接调用。

函数调用

当一个函数并非一个对象的属性时,那么它就是被当做一个函数来调用的。对于普通的函数调用来说,函数的返回值就是调用表达式的值

function sum (x, y) {return x + y;}
var total = sum(1, 2);
console.log(total); // 3

使用函数调用模式调用函数时,非严格模式下,this被绑定到全局对象;在严格模式下,thisundefined

// 非严格模式
function whatIsThis1() {console.log(this);
}
whatIsThis1(); // window

// 严格模式
function whatIsThis2() {
  'use strict';
  console.log(this);
}
whatIsThis2(); // undefined

方法调用

当一个函数被保存为对象的一个属性时,称为方法,当一个方法被调用时,this被绑定到该对象。

function printValue(){console.log(this.value);  
}
var value=1;
var myObject = {value:2};
myObject.m = printValue;
// 作为函数调用
printValue();
// 作为方法调用
myObject.m();

咱们注意到,当调用 printValue 时,this绑定的是全局对象 (window),打印全局变量value1。但是当调用 myObject.m() 时,this绑定的是方法 m 所属的对象 Object,所以打印的值为Object.value,即2

构造函数调用

如果函数或者方法调用之前带有关键字new,它就构成构造函数调用。

function fn(){this.a = 1;};
var obj = new fn();
console.log(obj.a);//1

参数处理:一般情况构造器参数处理和函数调用模式一致。但如果构造函数没用形参,JavaScript 构造函数调用语法是允许省略实参列表和圆括号的。

如:下面两行代码是等价的。

var o = new Object();
var o = new Object;

函数的调用上下文为新创建的对象。

function Book(bookName){this.bookName = bookName;}
var bookName = 'JS 深入浅出';
var book = new Book('ES6 深入浅出');
console.log(bookName);// JS 深入浅出
console.log(book.bookName);// ES6 深入浅出
Book('新版 JS 深入浅出');
console.log(bookName); // 新版 JS 深入浅出
console.log(book.bookName);// ES6 深入浅出

1. 第一次调用 Book() 函数是作为构造函数调用的,此时调用上下文 this 被绑定到新创建的对象,即 book。所以全局变量 bookName 值不变,而 book 新增一个属性bookName,值为'ES6 深入浅出'

2. 第二次调用 Book() 函数是作为普通函数调用的,此时调用上下为 this 被绑定到全局对象,在浏览器中为 window。所以全局对象的bookNam 值改变为 '新版 JS 深入浅出',而book 的属性值不变。

间接调用

JS 中函数也是对象,函数对象也可以包含方法,call()apply() 方法可以用来间接地调用函数。

这两个方法都允许显式指定调用所需的 this 值,也就是说,任何函数可以作为任何对象的方法来调用,哪怕这个函数不是那个对象的方法。两个方法都可以指定调用的实参。call()方法使用它自有的实参列表作为函数的实参,apply()方法则要求以数组的形式传入参数。

var obj = {};
function sum(x,y){return x+y;}
console.log(sum.call(obj,1,2));//3
console.log(sum.apply(obj,[1,2]));//3

词法 (静态) 作用域与动态作用域

作用域

通常来说,一段程序代码中所用到的名字并不总是有效 / 可用的,而限定这个名字的可用性的代码范围就是这个名字的作用域。

词法作用域

词法作用域 ,也叫 静态作用域,它的作用域是指在词法分析阶段就确定了,不会改变。而与词法作用域相对的是动态作用域,函数的作用域是在函数调用的时候才决定的。

来个例子,如下代码所示:

var blobal1 = 1;
function fn1 (param1) {
  var local1 = 'local1';
  var local2 = 'local2';
  
  function fn2(param2) {
    var local2 = 'inner local2';
    console.log(local1)
    console.log(local2)
  }

  function fn3() {
    var local2 = 'fn3 local2';
    fn2(local2)
  }
  fn3()}

fn1()

当浏览器看到这样的代码,不会马上去执行,它会先生成一个 抽象语法树。上述代码生成的抽象语法树大概是这样的:

执行 fn1 函数,fn1中调用 fn3(),从 fn3 函数内部查找是否有局部变量 local1,如果没有,就根据抽象树,查找上面一层的代码,也就是 local1 等于 'local1',所以结果会打印 'local1'

同样的方法查找是否有局部变量 local2,发现当前作用域内有 local2 变量,所以结果会打印 'inner local2

思考

有如下的代码:

var a = 1;
function fn() {console.log(a)
}

两个问题:

  1. 函数 fn 里面的变量 a, 是不是外面的变量 a
  2. 函数 fn 里面的变量 a的值, 是不是外面的变量 a的值。

对于第一个问题:

分析一个语法,就能确定函数 fn里面的 a 就是外面的 a

对于第二个问题:

函数 fn 里面的变量 a的值, 不一定是外面的变量 a的值,假设咱们这样做:

var a = 1;
function fn() {console.log(a)
}
a = 2
fn()

这时候当咱们执行 fn() 的时候,打印 a 的值为 2。所以如果没有看到最后,一开始咱们是不知道打印的 a 值到底是什么。

所以词法作用域只能确定变量所在位置,并不能确定变量的值。

调用栈(Call Stack)

什么是执行上下文

执行上下文就是当前 JavaScript 代码被解析和执行是所在环境的抽象概念,JavaScript 中运行任何的代码都是在执行上下文中运行。

执行上下文的类型,主要有两类:

  • 全局执行上下文 :这是默认的,最基础的执行上下文。不在任何函数中的代码都位于全局执行上下文中。共有两个过程:1. 创建有全局对象,在浏览器中这个全局对象就是window 对象。2. 将 this 指针指向这个全局对象。一个程序中只能存在一个执行上下文。
  • 函数执行上下文:每次调用函数时,都会为该函数创建一个新的执行上下文。每个函数都拥有自己的执行上下文,但是只有在函数被调用的时候才会被创建。一个程序中可以存在多个函数执行上下文,这些函数执行上下文按照特定的顺序执行一系列步骤,后文具体讨论。

调用栈

调用栈,具有LIFO(Last in, First out 后进先出)结构,用于存储在代码执行期间创建的所有执行上下文。

当 JavaScript 引擎首次读取脚本时,会创建一个全局执行上下文并将其 push 到当前执行栈中。每当发生函数调用时,引擎都会为该函数创建一个新的执行上下文并 push 到当前执行栈的栈顶。

引擎会运行执行上下文在执行栈栈顶的函数,根据 LIFO 规则,当此函数运行完成后,其对应的执行上下文将会从执行栈中 pop 出,上下文控制权将转到当前执行栈的下一个执行上下文。

看看下面的代码:

var myOtherVar = 10;

function a() {console.log('myVar', myVar);
  b();}

function b() {console.log('myOtherVar', myOtherVar);
  c();}

function c() {console.log('Hello world!');
}

a();

var myVar = 5;

有几个点需要注意:

  • 变量声明的位置(一个在上,一个在下)
  • 函数 a 调用下面定义的函数 b, 函数b 调用函数c

当它被执行时你期望发生什么?是否发生错误,因为 ba之后声明或者一切正常?console.log 打印的变量又是怎么样?

以下是打印结果:

"myVar" undefined
"myOtherVar" 10
"Hello world!"

1. 变量和函数声明(创建阶段)

第一步是在内存中为所有变量和函数分配空间。但请注意,除了 undefined 之外,尚未为变量分配值。因此,myVar在被打印时的值是undefined,因为 JS 引擎从顶部开始逐行执行代码。

函数与变量不一样,函数可以一次声明和初始化,这意味着它们可以在任何地方被调用。

所以以上代码在创建阶段时,看起来像这样子:

var myOtherVar = undefined
var myVar = undefined

function a() {...}
function b() {...}
function c() {...}

这些都存在于 JS 创建的全局上下文中,因为它位于全局作用域中。

在全局上下文中,JS 还添加了:

  • 全局对象(浏览器中是 window 对象,NodeJs 中是 global 对象)
  • this 指向全局对象

2. 执行

接下来,JS 引擎会逐行执行代码。

myOtherVar = 10在全局上下文中,myOtherVar被赋值为10

已经创建了所有函数,下一步是执行函数 a()

每次调用函数时,都会为该函数创建一个新的上下文(重复步骤 1),并将其放入调用堆栈。

function a() {console.log('myVar', myVar)
  b()}

如下步骤:

  • 创建新的函数上下文
  • a 函数里面没有声明变量和函数
  • 函数内部创建了 this 并指向全局对象(window)
  • 接着引用了外部变量 myVarmyVar 属于全局作用域的。
  • 接着调用 函数 b 函数 b 的过程跟 a 一样,这里不做分析。

下面调用堆栈的执行示意图:

  • 创建全局上下文,全局变量和函数。
  • 每个函数的调用,会创建一个上下文, 外部环境的引用及 this。
  • 函数执行结束后会从堆栈中弹出,并且它的执行上下文被垃圾收集回收(闭包除外)。
  • 当调用堆栈为空时,它将从事件队列中获取事件。

代码部署后可能存在的 BUG 没法实时知道,事后为了解决这些 BUG,花了大量的时间进行 log 调试,这边顺便给大家推荐一个好用的 BUG 监控工具 Fundebug。

交流

阿里云最近在做活动,低至 2 折,有兴趣可以看看:https://promotion.aliyun.com/…

干货系列文章汇总如下,觉得不错点个 Star,欢迎 加群 互相学习。

https://github.com/qq449245884/xiaozhi

因为篇幅的限制,今天的分享只到这里。如果大家想了解更多的内容的话,可以去扫一扫每篇文章最下面的二维码,然后关注咱们的微信公众号,了解更多的资讯和有价值的内容。

每次整理文章,一般都到 2 点才睡觉,一周 4 次左右,挺苦的,还望支持,给点鼓励

正文完
 0