函数

在JavaScript中函数创建时,存储在堆内存中,存储的是它的执行代码,而且是以“字符串”的形式存储的;函数执行时,形成私有上下文和块级作用域,保证其执行过程不会直接影响到全局。

JS中的函数有以下五大要素:

  1. 执行主体
  2. 作用域链
  3. 形参变量
    1. 用户传递的形参
    2. 初始化arguments
    3. 数组解构
  4. 私有上下文
  5. 返回值

JS中的函数种类:

  1. 普通函数:function fn(){}
  2. 匿名函数:~function (){}()
  3. 箭头函数:()=>{}
  4. 构造函数
  5. 生成器函数

执行主体this

this是函数的执行主体,大致意思就是谁执行了函数

按照以下规律来确定执行主体是谁:

  1. 给当前元素的某个事件行为绑定方法,事件触发,执行对应的方法,方法中的this是当前元素本身(排除∶IE6~8基于attachEvent实现的DOM2事件绑定,绑定的方法中的this不是操作的元素,而是window)
  2. 函数执行,首先看函数名之前是否有“点””,有“点”,“点”前面是谁this就是谁,没有“点”this就是window(在J5的严格模式下,没有“点”,方法中的this是undefined)
    • 自执行函数中的this一般都是window/undefined
    • 回调函数中的this一般也都是window/undefined(除非特殊处理了)+…
  3. 构造函数中的this是当前类的实例
  4. 箭头函数没有自己的this,用到的this都是上下文中的this
  5. 基于call/apply/bind可以强制改变this的指向

[ 1 ] 例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
console.log(this); // ==> window
{
console.log(this); // ==> 块级上下文没有this,输出的是上级上下文的this
}

(function(){
console.log(this); // this ===> window
})();



// 回调函数: 把一个函数中作为实参值,传递给另外一个函数

function fn(callback){
callback(); // 函数执行前没有“点”,这里的this指代window
}

let obj = {
sum(){
console.log(this); // this 需要函数执行时才能被确定
}
}
fn(obj.sum);

[ 2 ] 例 特殊处理的this

1
2
3
4
5
6
7
8
9
10
11
let obj = {
name:'xxx'
};
let arr = [10,20];
arr.forEach(function(item,index){
console.log(this); // this ==> obj
},obj); // 因为出发回调函数执行的时候,forEach内部会把回调函数中的this改变为传递的第二个参数值obj “ 特殊处理 ”

// 括号表达式
(obj.sum)() // this->obj
(10,obj.sum)() // this -> window 括号表达式:小括号中包含多项【只有一项写和不写没有区别】,只取最后一项,但是这样处理后this会发生改变,变为window/undefined

arguments

实参集合

1
2
3
4
5
6
7
8
9
10
function fn(a,b,c){
console.log(arguments);
}
/* arguments内容
a:
b:
c:
length: //实参长度
callee: f b(){...} //指代当前函数本身
*/
  • 在JS的非严格模式下,当“初始化arguments”和“形参赋值“完成后,会给两者建立一个映射机制,一个修改另一个也会跟着改

  • 在JS严格模式下,没有映射机制,也没有arguments.callee这个属性;箭头函数中没有arguments

  • arguments.callee()调用函数执行本身时,this指向arguments

作用域链查找

私有上下文中代码执行,如果遇到一个变量,先看是否为自己私有变量,如果是私有的,则操作自己的,和外界没有关系;如果不是自己私有的,则基于作用域链向其上下文中查找,看是否为上级上下文中私有的,如果也不是,则继续向上查找,一直找到EC(G)全局上下文为止,我们把这种查找过程称之为 [ 作用域链查找机制 ]

函数执行上下文只与函数创建的位置有关,和在哪执行没有关系

函数的执行步骤

  1. 形成一个私有的上下文(AO私有变量对象等,存储当前上下文中声明的变量),然后进栈执行
  2. 代码执行之前
    • 初始化作用域链 scope-chain,当前自己的私有上下文,函数的作用域(创建函数时所在的上下文),链的右侧是当前上下文的上级上下文
    • 初始化this
    • 初始化arguments
    • 形参赋值:在当前上下文中声明一个形参变量,并把传递的实参赋值给它
    • 变量提升
  3. 代码执行
  4. 出栈释放

函数执行会形成一个私有的上下文,里面的变量受到上下文的保护, 不受外界干扰;

私有上下文有可能形成不被释放的上下文,里面的私有变量和值就会被保存起来,这些变量和值可以供下级上下文使用

我们把函数的这种保存和保护的机制称为闭包,简而言之即形成不被释放的上下文

需要注意的是,声明函数时创建的堆内存和执行函数时生成的函数执行上下文并不是同一个东西,有时候堆内存释放了但函数执行上下文还保留着。

[ 1 ]

1
2
3
4
5
6
7
8
9
var x = [12,23];
function fn(y){
y[0] = 100; // [100,23]
y = [100]; // [100]
y[1] = 200; // [100,200]
console.log(y); // [100,200]
}
fn(x);
console.log(x); // [100,23]

[ 2 ]

1
2
3
4
5
6
7
8
9
10
11
12
let x= 5;
function fn(x){
return function(y){
console.log(`y: ${y}\t x: ${x}`)
console.log(y+(++x))
}
}
let f = fn(6)
f(7)
fn(8)(9)
f(10)
console.log(x);

[ 3 ]

1
2
3
4
5
6
7
8
9
let a = 0,b=0;
function A(a){
A = function(b){
alert(a+b++);
}
alert(a++)
}
A(1);
A(2);

[ 4 ]

1
2
3
4
5
6
7
8
9
10
11
function fun(n,o){
console.log(o);
return {
fun: function(m){
return fun(m,n)
}
}
}
var c = fun(0).fun(1)
c.func(2);
c.func(3);

垃圾回收

函数执行的时候需要进栈,如果有很多函数,浏览器就需要很多内存,这时候就需要有相应的机制回收执行完的函数

浏览器垃圾回收机制 / 内存(堆内存和栈内存)释放机制 GC

1
2
3
4
5
6
7
8
9
1. 栈内存的释放
加载页面会形成一个全局的上下文,只有页面关闭后,全局上下文才会被释放,函数执行会形成一个私有的上下文,进栈执行,当函数中代码执行完成,大部分情况,形成的上下文都会被出栈释放掉,以此优化栈内存的大小

2. 堆内存的释放
常用的内存管理机制:关联引用(chrome),引用计数(IE)
关联引用:浏览器在空闲或者指定时间内,查找所有的堆内存,并把没有被任何东西占用的堆内存释放掉
引用计数:创建堆内存,被占用一次,则计数器+1,取消占用则计数器-1,当计数器为0时,内存释放

如果要想内存直接释放可以选择:variable = null;

箭头函数

箭头函数是ES6新增的一种写法,主要是为了简化函数的写法,下面展示一下普通函数在ES5中的写法和ES6中的写法的对比。

ES5:

1
2
3
4
5
function func(x){
return function(y){
return x+y;
}
}

ES6:

1
let func = x => y => x+y;

如果函数参数只有一个值,那么包裹参数的小括号可以省略;并且如果函数体中只有一句return,则return也可以省略

一般箭头函数的语法:

1
2
3
4
5
6
7
8
9
10
11
12
function func(x,y){
let sum = 0;
sum = x + y;
return sum;
}

// ----
(x,y)=>{
let sum = 0;
sum = x + y;
return sum;
}

箭头函数和普通函数的区别:

  • 箭头函数中没有arguments,而普通函数中,arguments是传入的参数形成的数组
  • 箭头函数中this指向undefined,而普通函数中,this指向调用的对象或者是window

匿名函数

匿名函数一:向其他函数中传入函数

1
2
3
setInterval(function(){
console.log(1);
},100)

匿名函数二:自执行函数

1
2
3
(function(i){
console.log(i);
})(1)

匿名函数具名化,就是给匿名函数设置一个名字,需要注意的是:

  1. 此时这个名字可以在当前函数形成的私有上下文中使用,代表当前函数本身
  2. 此名字不能再外部的上下文中使用
  3. 在本函数中使用时,它的值是不允许被修改的
  4. 如果当前的名字被上下文中的其他变量声明过,它的值是可以改动的,名字是私有变量
1
2
3
4
5
6
7
8
9
10
11
12
(function b(){
console.log(b) // b是匿名函数本身
b = 100
console.log(b) // b是匿名函数本身
})

(function b(){
console.log(b) // undefined
var b = 100
console.log(b) // 100
})

构造函数

普通函数可以直接函数名+小括号的方式执行,其含义相当于在当前上下文执行一遍函数体中的语句,函数本身存在的目的也是为了代码的重用。而同样的,构造函数执行也会执行一遍函数体中的内容,不同的是,构造函数是new 函数名执行的,浏览器执行构造函数时会创建一个实例对象,且构造函数中this指向当前实例对象。

构造函数 VS 普通函数区别详解:

  1. 构造函数执行,也会像普通函数执行一样,形成私有的上下文
    • 初始化SCOPE_CHAIN
    • 形参赋值
    • 变量提升
  2. 不同的地方:
    • 创建上下文之后,浏览器默认会帮助我们创建一个“实例对象”;把Fn函数当作一个类“构造函数”, 创建的对象就是这个类的实例
    • 初始this的时候,让this指向当前创建的实例对象
    • 在代码执行完,返回值的时候,如果函数没有写return,或者返回的是基本数据类型,则浏览器默认会把创建的实例对象返回;如果函数本身返回的是一个引用数据类型,则返回引用数据类型

[ 1 ] 例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function Fn(x, y) {
let total = x + y;
this.x = x;
this.y = y;
return total;
}
// 作为普通函数执行
Fn(10, 20);

// 作为构造函数执行
let f1 = new Fn(10, 20)
// console.log(f1.x,f1.y,f1.total); // 10 20 undefined
console.log(f1)

// 检测当前实例是否属于这个类,如果返回的是引用类型,则不属于这个类
console.log(
f1 instanceof Fn
)

// Fn VS Fn(): Fn代表函数本身,而Fn()是把函数执行
// new Fn VS new Fn(): 都是执行Fn,只是第一个不可以传参,第二个可以传参
// new Fn:运算优先级18
// new Fn(): 运算优先级19

生成器函数

生成器函数可以控制一个函数的暂停和继续,如下代码:

1
2
3
4
5
6
7
8
9
10
11
function* fn() {
let n = yield 1;
console.log(n); // name
yield 2;
console.log('A') // A
}

let f = fn();
console.log(f.next()); /* {value: 1, done: false} */
console.log(f.next('name')); /* {value: 2, done: false} */
console.log(f.next()); /* {value: undefined, done: true} */
  • f.next() 会返回一个object对象,键值对为: { value: , done: } ,其中Value就是yield的返回值,而done表示迭代是否执行完,如果没有执行完,则为false
  • f.next()可以接受一个参数,这个参数就是yield执行的返回值
  • fn()生成器函数本身无法被new执行
  • f.__proto__返回值是Generator的一个实例

[ 2 ] 例 基于生成器的斐波那契数列

1
2
3
4
5
6
7
8
9
10
11
12
function *fibonacciSequence() {  
let x = 0, y = 1;
for(;;) {
yield y;
[x, y] = [y, x+y];
}}

function fibonacci(n) {
for(let f of fibonacciSequence()){
if (n-- <= 0) return f;
}}
console.log(fibonacci(20)) // => 10946

原型和原型链

对于其他后端语言来说,面向对象的实现、封装、继承、多态的实现,通常是采用类 ( Class ) 的方式,然而Javascript语言本身并不提供类实现,即使ES6中引入了Class,但它(Class)本身还是一个基于原型的语法糖。

当谈到继承时,JavaScript 只有一种结构:对象。每个实例对象( object )都有一个私有属性(称之为 proto )指向它的构造函数的原型对象(prototype )。该原型对象也有一个自己的原型对象( proto ) ,层层向上直到一个对象的原型对象为 null。根据定义,null 没有原型,并作为这个原型链中的最后一个环节。

上面提到的构造函数,就是用于js继承的一种实现方式,构造函数的实例继承了构造函数原型上的方法,也可以调用原型上的方法(类似于公有方法)。举一个例子,Array数组有一系列的方法,slice,indexOf,filter等,这些方法都存在于数组这个原型上,实例通过原型链的方式进行继承。

image-20201213211628443

1
2
3
4
5
6
7
8
9
10
11
12
function Fn(){
this.Q = 'Que' // 实例的私有属性
console.log('A')
}
// 在原型上添加方法
Fn.prototype.showmsg = function showmsg(){
console.log('showmsg')
}

let f = new Fn()
f.showmsg() // 实例可以调用自己原型链上的方法 === f.__proto__.showmsg()
// f.__proto__ === Fn.prototype true

类就是构造函数的语法糖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
class Person {
// 初始化
constructor(fname, lname) {
this.firstName = fname; // 定义公有属性,实例的属性
this.lastName = lname;
this.fullName = `${this.firstName} ${this.lastName}`
}

B = 3; // 定义私有属性 相当于在constructor中写 this.B = 3
// 静态字段,new后不会放到实例上,但可以通过Person.A获取
static A = 1;

// 相当于在原型上添加方法
showmsg() {
console.log('show msg')
}


}

// 继承Person类
class Human extends Person {
constructor(fname, lname) {
// 调用父类的构造函数
super(fname, lname)
this.sex = 'human'
}
}
console.log(Human.A); // 1

var zjs = new Human('A', 'B');
console.log(zjs)
zjs.showmsg()

如何用构造函数实现类?

Reference

[ 1 ] https://developer.ibm.com/zh/technologies/web-development/articles/wa-es6-generator/

[ 2 ] https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Inheritance_and_the_prototype_chain