基础
1. 函数声明与函数表达式
在 javaScript
中,声明一个函数有两种方式,分别是函数声明和函数表达式
// 函数声明
function name(params) {
// function body
}
// 函数表达式
cosnt f = function(params) {
// function body
}
可以看到,函数声明就是直接使用 function
关键字 直接声明一个函数,而 函数表达式 则是将一个函数赋值给一个变量,这将导致在解析阶段的解析顺序不同,具体会在后面的 变量提升 处分析
当然,也可以通过 Function
构造函数创建新的函数,但是并不推荐,因为执行一次 new Function
,就会解析一次函数体,而函数声明或函数表达式不会,相同的函数体只会解析一次
2. 函数属性
2.1 name
函数的名字,匿名函数的名字是空字符串,函数表达式的函数名就是变量名,如果函数表达式的函数有具体名字,则为函数的具体名字
函数名只是指向函数的一个引用,不会与某个函数绑定
2.2 length
函数形参列表的个数,虽然表示函数形参列表的个数,但是 JavaScript 中,对传参的数量基本认为是没有限制的,即使没有在形参列表中声明,传递后在函数中也能访问到
2.3 prototype
函数的原型对象,使用面向对象编程时,在上面声明实例方法,所有 函数实例 共享
3. 函数方法
3.1 call、apply
这两个方法都可以改变函数在执行的时候,并且调用方法时会直接执行方法,函数内部的 this
指向,区别在于,call
方法必须将参数一个个的传入,而 apply
, 可以使用一个数组一次性将参数传入
3.2 bind
bind
方法用于将函数的 this
指向 绑定到指定的对象上,该方法返回一个绑定后的函数,传入无效值则 绑定为 window
对象
作用域
从字面上来看,作用域就好像是用来限制什么的一样,实际上,它的作用也是差不多的,作用域 就是存储变量的一套规则,用于方便的查询变量,其限定了变量的作用范围
函数的作用域在函数声明的时候就已经确定了,和其在什么地方调用无关,参考后面的执行环境部分
1. 变量声明
如果在函数内多次声明同一个变量,其实只会 声明一次,其余的声明会被忽略,但是 赋值并不会被忽略
function f() {
var a = 1;
console.log(a); // => 1
var a = 2;
console.log(a); // => 2
}
// 等同于下面的代码
function f() {
var a = 1;
console.log(a);
a = 2;
console.log(a);
}
当函数名和变量名相同了怎么办,一个变量只能有一个值,那么先保留函数还是先保留变量呢,看下面代码
function f() {
function a() {
console.log(1);
}
console.log(a); // 函数 a
var a = 2;
console.log(a); // 2
}
可以看到,函数是优先的
变量的声明规则:
- 变量提升,函数优先
- 发现已经有声明的变量名或者函数名,若是变量,则跳过当前声明,若是函数,则覆盖
变量提升
一般来讲,代码都是从上到下解析,但是在 JavaScript 中,某些情况下是不一样的,比如这里要讲的变量提升
所谓 变量提升,指的是,使用 var
关键字 定义变量、函数声明 定义函数的时候,会将 变量、函数 提升到 函数顶部 声明,记住,只有声明会被提前,赋值并不会
注意:只有使用 var
声明的变量会进行变量提升,使用 cosnt
、let
声明的变量并不会
1. 变量的提升
function f() {
a = 2;
var a;
console.log(a);
}
a() // => 2
function f1() {
console.log(a);
var a = 2;
}
f1() // => undefined
我们来看上面的两个函数,是变量的提升,至于打印的结果是为什么,我们根据前面的话来分析一下
首先,使用 var 变量定义的变量会被提升到函数顶部声明,那么,上面的代码其实就等于下面的代码:
function f() {
var a;
a = 2;
console.log(a);
}
function f1() {
var a;
console.log(a);
a = 2;
}
而且,变量提升只会 提升变量的声明,而不会提升赋值,所以赋值语句在原地等待从上到下解析执行,到这里,结果打印什么已经很明白了
如果不知道为什么打印 undefined
:在 JavaScript 中,声明了未赋值的变量的值都是 undefined
2. 函数的提升
函数和变量是不一样的,因为函数可以被执行,在提升到顶部以后,函数可以直接被执行
function f() {
console.log(a); // => 函数 a
a(); // => 1
function a() {
console.log(1);
}
}
// 提升后的代码
function f() {
function a() {
console.log(1);
}
console.log(a); // => 函数 a
a(); // => 1
}
但是,还记得我们开头的 函数声明和函数表达式 吗,既然 函数声明 会被提升,那么函数表达式呢,看下面的代码
function f() {
console.log(b);
b();
var b = function() {
console.log(2);
}
}
上面这段代码的结果是什么,会和之前一样,先打印 函数 b 的函数体,然后再打印 2 吗,可以尝试执行一下
是不是发现报错了,下面我们看一下提升之后的代码长什么样子:
function f() {
var b;
console.log(b);
b();
b = function() {
console.log(2);
}
}
现在知道为什么报错了吧,因为函数表达式是 声明一个变量,将匿名函数赋值给该变量,所以应用的是 变量的提升,所以函数表达式能够保证在函数创建以后才被执行,而不是像函数声明,可以声明在函数底部,而在函数顶部执行
3. 变量和函数重名
上面我们已经理解了变量和函数的提升,但是当变量名和函数名重复的时候,该怎么提升呢,是以谁为准呢,看下面代码
function f() {
console.log(a); // 1
var a = 1;
console.log(a); // 2
function a() {
console.log(2);
}
}
执行一下上面的代码,会发现 1 处 打印的是 函数 a 的函数体,这就说明了,先提升函数,而 2 处 又打印了 1,说明 赋值并不会被提升,只有声明会被提升
执行环境(执行上下文)
每个函数都有与之对应的执行环境,全局的执行环境就是 window
对象,执行环境中包含三个重要的东西:变量对象、作用域链、this
执行环境在函数声明的时候就已经被创建
1. 变量对象
变量对象中包含的是当前函数作用域内声明的 变量、函数、形参 以及 arguments
对象
所以之前在作用域那里说的:作用域是存储变量的规则 – 而变量对象就是其实现
在函数刚声明创建的时候,变量对象中只有 arguments
对象,并且其值为 null
,我们可以通过 Chrome 断点看一下:
可以看到,当声明了 函数 a 之后,还没有执行的时候,函数 a 的 arguments
对象已经存在,并且值为 null
,这就证明了执行环境是在函数创建的时候就被创建了
在函数执行的过程中,变量对象渐渐被函数内声明的变量填充,变得有货了,就转变成为了 活动对象,所以,变量对象 和 活动对象 其实本质上是一个对象,只是在函数不同阶段的状态,看下图:
可以看到函数执行的时候,arguments
对象已经有值了,变量也有值了,当然,上图是执行完函数之后,如果断点进行到刚开始执行的时候,变量的值是 undefined
,因为毕竟还没有执行赋值语句
当函数执行完毕,当前的函数的活动对象会被销毁,然后函数被弹出函数执行栈,所以函数执行完毕,函数的活动对象又会变回成函数的变量对象
2. 作用域链
我们知道,当访问一个变量的时候,会先在当前函数作用域中查找,如果没找到,就会去它的外层函数作用域再查找,直到全局作用域,相信大家已经猜到了,变量查找通过的就是作用域链,而通过前面的介绍我们也知道了作用域的具体实现就是 变量对象,所以作用域链的本质就是 函数的活动对象的集合,只不过这个集合是有顺序的,当前正在执行的函数的活动对象,一定在作用域链的最前端
在函数刚声明创建的时候,会创建作用域链,只不过这时候这里只包含外层函数的 活动对象,如下图:
当函数执行的时候,复制一份创建的作用域链,并且将当前函数的 活动对象 放到作用域链的前端,这时就构成了当前函数的作用域链,访问变量的时候,通过作用域链一层层的向上查询就可以了
3. this
在函数创建的时候,并没有 this
变量,只有当函数执行的时候,才会初始化 this
指向
函数的 this
指向可以通过 call、apply、bind
方法改变