阿里云最近在做活动,低至 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
被绑定到全局对象;在严格模式下,this
是undefined
// 非严格模式
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),打印全局变量value
值1
。但是当调用 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)
}
两个问题:
- 函数
fn
里面的变量a
, 是不是外面的变量a
。 - 函数
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
当它被执行时你期望发生什么?是否发生错误,因为 b
在a
之后声明或者一切正常?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) - 接着引用了外部变量
myVar
,myVar
属于全局作用域的。 - 接着调用
函数 b
,函数 b
的过程跟a
一样,这里不做分析。
下面调用堆栈的执行示意图:
- 创建全局上下文,全局变量和函数。
- 每个函数的调用,会创建一个上下文, 外部环境的引用及 this。
- 函数执行结束后会从堆栈中弹出,并且它的执行上下文被垃圾收集回收(闭包除外)。
- 当调用堆栈为空时,它将从事件队列中获取事件。
代码部署后可能存在的 BUG 没法实时知道,事后为了解决这些 BUG,花了大量的时间进行 log 调试,这边顺便给大家推荐一个好用的 BUG 监控工具 Fundebug。
交流
阿里云最近在做活动,低至 2 折,有兴趣可以看看:https://promotion.aliyun.com/…
干货系列文章汇总如下,觉得不错点个 Star,欢迎 加群 互相学习。
https://github.com/qq449245884/xiaozhi
因为篇幅的限制,今天的分享只到这里。如果大家想了解更多的内容的话,可以去扫一扫每篇文章最下面的二维码,然后关注咱们的微信公众号,了解更多的资讯和有价值的内容。
每次整理文章,一般都到 2 点才睡觉,一周 4 次左右,挺苦的,还望支持,给点鼓励