一、函数的定义
1.1 函数声明和函数表达式
定义函数常用的两种方式是函数声明和函数表达式,如下:
function fn1 () { //函数声明
console.log('fn1()')
}
var fn2 = function () { //表达式
console.log('fn2()')
}
关于函数声明与函数表达式的区别,请参考大神之作:https://www.cnblogs.com/chaoyuehedy/p/9110063.html
1.2 函数的调用
函数调用方式有以下几种:
- 直接调用: test()
- 通过对象调用: obj.test()
- new调用: new test()
临时让test成为obj的方法进行调用:test.call/apply(obj)
1.3 回调函数
简单理解什么函数才是回调函数?
你定义的
- 你没有调
- 但最终它执行了(在某个时刻或某个条件下)
常见的回调函数:
- dom事件回调函数 ==>发生事件的dom元素
- 定时器回调函数 ===>window
- ajax请求回调函数
-
1.4 IIFE(立即执行函数)
全称: Immediately-Invoked Function Expression,即立即执行函数。常用的写法如下:
(function () { //匿名函数自调用
var a = 3
console.log(a + 3)
})()
如上的代码理解为(匿名函数)(),即定义一个匿名函数后直接使用 () 来调用该匿名函数。更多关于立即执行函数的知识,参考:https://www.cnblogs.com/cnfxx/p/7337889.html
立即执行函数的作用: 隐藏实现,其内部的变量对外是不可见的(函数局部变量,作用域为函数体内),是变量更安全
- 不会污染外部(全局)命名空间,内部变量时局部变量,不会造成全局变量的更改
-
1.5 函数中的this
1.5.1 this是什么?
任何函数本质上都是通过某个对象来调用的,如果没有直接指定就是window
- 所有函数内部都有一个变量this
-
1.5.2 如何确定this的值? 记住以下几点
test(): window
- p.test(): p
- new test(): 新创建的对象
p.call(obj): obj
了解更多关于JavaScript this 的知识请参考:https://www.cnblogs.com/yuanbo88/p/6290543.html
二、原型与原型链
2.1 原型(prototype)
2.1.1 函数的prototype属性
每个函数都有一个prototype属性, 它默认指向一个Object空对象(即称为: 原型对象)
原型对象中有一个属性constructor, 它指向函数对象。如下:
function Fun () {}
console.log(Fun.prototype) // 默认指向一个Object空对象(没有我们的属性)
//控制台输出结果为:
{constructor: ƒ}
constructor: ƒ Fun()
__proto__: Object
2.1.2 给原型对象添加属性(一般都是方法)
给对象原型添加方法直接使用:Fun.prototype.方法名 = function() {方法体}。作用:函数的所有实例对象自动拥有原型中的属性(方法)
Fun.prototype.test = function () {
console.log('test()')
}
var fun = new Fun()
fun.test() //通过实例化对象调用原型对象中的方法。此处的方法类似于java中的实例方法
以上代码在控制台输出 : test()。即通过给函数的原型对象添加的方法,函数的实例对象会自动拥有该方法。若要给函数添加静态方法,代码如下:
Fun.staticFun = function() { //给函数对象添加静态的方法
console.log( 'this is static function')
}
Fun.staticFun(); //通过函数名直接调用静态方法,类似java中的静态方法的调用
2.2 显式原型与隐式原型
- 每个函数function都有一个prototype,即显式原型(属性)
- 每个实例对象都有一个proto,可称为隐式原型(属性)
- 对象的隐式原型的值为其对应构造函数的显式原型的值 ```javascript //定义构造函数 function Fn() { // 内部语句: this.prototype = {}
} console.log(Fn.prototype) //定义函数时添加了显示原型即prototype属性
var fn = new Fn() // 内部语句: this.proto = Fn.prototype console.log(fn.proto) //创建对象时给对象添加了隐式原型属性proto
console.log(Fn.prototype===fn.proto) // true 对象的隐式原型与其构造函数的显示原型是同一个对象
**总结:**<br />* 函数的prototype属性: 在定义函数时自动添加的, 默认值是一个空Object对象<br />* 对象的__proto__属性: 创建对象时自动添加的, 默认值为构造函数的prototype属性值<br />* 程序员能直接操作显式原型, 但不能直接操作隐式原型(ES6之前)
<a name="Apsn7"></a>
### 2.3 原型链
<a name="7vYqJ"></a>
#### 2.3.1 原型链(图解)。
创建一个Fun函数对象,并new创建一个Fun的实例对象,访问对象属性是的分析图如下。访问一个对象的属性时:
1. 先在自身属性中查找,找到返回;
1. 如果没有, 再沿着__proto__这条链向上查找, 找到返回;
1. 如果最终没找到, 返回undefined
![1667240-20190928160823167-1006516401.png](https://cdn.nlark.com/yuque/0/2020/png/1121446/1585465681557-1b7f1653-7618-421a-bf21-972387de8954.png#align=left&display=inline&height=513&name=1667240-20190928160823167-1006516401.png&originHeight=513&originWidth=1141&size=53093&status=done&style=none&width=1141)
<a name="PKfuh"></a>
#### 2.3.2 原型链_属性问题
1. 读取对象的属性值时: 会自动到原型链中查找
1. 设置对象的属性值时: 不会查找原型链, 如果当前对象中没有此属性, 直接添加此属性并设置其值
1. 方法一般定义在原型中, 属性一般通过构造函数定义在对象本身上
```javascript
function Fn() {
}
Fn.prototype.a = 'xxx'
var fn1 = new Fn()
console.log(fn1.a, fn1) //'xxx'
var fn2 = new Fn()
fn2.a = 'yyy'
console.log(fn1.a, fn2.a, fn2) //'xxx' 'yyy'
2.3.3 instanceof是如何判断的
表达式: A instanceof B, 如果B函数的显式原型对象在A对象的原型链上, 返回true, 否则返回false
function Foo() { }
var f1 = new Foo()
console.log(f1 instanceof Foo) // true
console.log(f1 instanceof Object) // true
console.log(Object instanceof Function) // true
console.log(Object instanceof Object) // true
console.log(Function instanceof Function) // true
console.log(Function instanceof Object) // true
function Foo() {}
console.log(Object instanceof Foo) // false
三、执行上下文
3.1 引入
3.1.1 变量声明提升
通过var定义(声明)的变量, 在定义语句之前就可以访问到,其值为: undefined
var a = 3
function fn () {
console.log(a) //输出结果为undefined
var a = 4
}
fn()
3.1.2 函数声明提升
通过function声明的函数, 在之前就可以直接调用,其值为: 函数定义(对象)
console.log(b) //undefined 变量提升
fn2() //可调用 函数提升
// fn3() //不能 变量提升
var b = 3
function fn2() {
console.log('fn2()')
}
var fn3 = function () {
console.log('fn3()')
}
更多关于变量提升和函数提升的知识见https://www.cnblogs.com/echolun/p/7612142.html
3.2 执行上下文
执行上下文是一个比喻的词,用于描述运行Javascript代码的环境。JavaScript的代码按位置可简单分为全局代码和函数(局部)代码。在JavaScript中,每次运行一些Javascript代码时,引擎都会创造一个全局执行上下文,当调用函数时,在执行函数体之前,JavaScript引擎会创建一个局部的执行上下文。
3.2.1 全局执行上下文
执行全局代码的过程:
- 在执行全局代码前将window确定为全局执行上下文
- 对全局数据进行预处理
- var定义的全局变量==>undefined, 添加为window的属性
- function声明的全局函数==>赋值(fun), 添加为window的方法
- this==>赋值(window)
-
3.2.2 函数执行上下文
执行局部(函数)代码的过程
在调用函数, 准备执行函数体之前, 创建对应的函数执行上下文对象(虚拟的, 存在于栈中)
- 对局部数据进行预处理
- 申明形参变量,并将实参变量的值赋值为形参变量,添加为执行上下文的属性
- arguments==>赋值(实参列表), 添加为执行上下文的属性
- var定义的局部变量==>undefined, 添加为执行上下文的属性
- function声明的函数 ==>赋值(fun), 添加为执行上下文的方法
- this==>赋值(调用函数的对象)
-
3.3 执行上下文栈
在全局代码执行前, JS引擎就会创建一个栈来存储管理所有的执行上下文对象(栈结构不了解的自行查询资料)。
在全局执行上下文(window)确定后, 将其添加到栈中(压栈)
- 在函数执行上下文创建后, 将其添加到栈中(压栈)
- 在当前函数执行完后,将栈顶的对象移除(出栈)
- 当所有的代码执行完后, 栈中只剩下window
如下代码执行的过程中会出现的上下文栈的变化图所示:
//创建全局上下文,并将该全局上下文储存到上下文栈中
var a = 10;
var bar = function (x) {
var b = 5;
//创建foo的局部上下文栈,并将该上下文对象存储到栈中,
//当函数foo执行完成后自动释放该执行上下文,跳到bar执行上下文中
foo(x + b);
}
var foo = function (y) {
var c = 5;
console.log(a + c + y);
}
bar(10) //创建bar的局部上下文对象,并将该上下文对象存储到栈中
试题:
查看如此代码,分析输出内容
console.log('gb: '+ i)
var i = 1
foo(1)
function foo(i) {
if (i == 4) {
return
}
console.log('fb:' + i)
foo(i + 1) //递归调用: 在函数内部调用自己
console.log('fe:' + i)
}
console.log('ge: ' + i)
答案:
gb: undefined
fb: 1
fb: 2
fb: 3
fe: 3
fe: 2
fe: 1
ge: 1
四、作用域和作用域链
4.1 作用域
4.1.1 理解:
- 区别1:创建的时间不同
- 全局作用域之外,每个函数都会创建自己的作用域,作用域在函数定义时就已经确定了。而不是在函数调用时
- 全局执行上下文环境是在全局作用域确定之后, js代码马上执行之前创建
- 函数执行上下文是在调用函数时, 函数体代码执行之前创建
- 区别2:执行上下文是动态的,作用域是静态的
- 作用域是静态的, 只要函数定义好了就一直存在, 且不会再变化
- 执行上下文是动态的, 调用函数时创建, 函数调用结束时就会自动释放
联系
在当前作用域下的执行上下文中查找对应的属性, 如果有直接返回, 否则进入2
- 在上一级作用域的执行上下文中查找对应的属性, 如果有直接返回, 否则进入3
- 再次执行2的相同操作, 直到全局作用域, 如果还找不到就抛出找不到的异常
内容来源于谷粒学院:http://www.gulixueyuan.com/my/course/194