函数
在JavaScript中函数创建时,存储在堆内存中,存储的是它的执行代码,而且是以“字符串”的形式存储的;函数执行时,形成私有上下文和块级作用域,保证其执行过程不会直接影响到全局。
JS中的函数有以下五大要素:
- 执行主体
- 作用域链
- 形参变量
- 用户传递的形参
- 初始化arguments
- 数组解构
- 私有上下文
- 返回值
JS中的函数种类:
- 普通函数:
function fn(){}
- 匿名函数:
~function (){}()
- 箭头函数:
()=>{}
- 构造函数
- 生成器函数
执行主体this
this是函数的执行主体,大致意思就是谁执行了函数
按照以下规律来确定执行主体是谁:
- 给当前元素的某个事件行为绑定方法,事件触发,执行对应的方法,方法中的this是当前元素本身(排除∶IE6~8基于attachEvent实现的DOM2事件绑定,绑定的方法中的this不是操作的元素,而是window)
- 函数执行,首先看函数名之前是否有“点””,有“点”,“点”前面是谁this就是谁,没有“点”this就是window(在J5的严格模式下,没有“点”,方法中的this是undefined)
- 自执行函数中的this一般都是window/undefined
- 回调函数中的this一般也都是window/undefined(除非特殊处理了)+…
- 构造函数中的this是当前类的实例
- 箭头函数没有自己的this,用到的this都是上下文中的this
- 基于call/apply/bind可以强制改变this的指向
[ 1 ] 例
1 | console.log(this); // ==> window |
[ 2 ] 例 特殊处理的this
1 | let obj = { |
arguments
实参集合
1 | function fn(a,b,c){ |
在JS的非严格模式下,当“初始化arguments”和“形参赋值“完成后,会给两者建立一个映射机制,一个修改另一个也会跟着改
在JS严格模式下,没有映射机制,也没有arguments.callee这个属性;箭头函数中没有arguments
arguments.callee()调用函数执行本身时,this指向arguments
作用域链查找
私有上下文中代码执行,如果遇到一个变量,先看是否为自己私有变量,如果是私有的,则操作自己的,和外界没有关系;如果不是自己私有的,则基于作用域链向其上下文中查找,看是否为上级上下文中私有的,如果也不是,则继续向上查找,一直找到EC(G)全局上下文为止,我们把这种查找过程称之为 [ 作用域链查找机制 ]
函数执行上下文只与函数创建的位置有关,和在哪执行没有关系
函数的执行步骤
- 形成一个私有的上下文(AO私有变量对象等,存储当前上下文中声明的变量),然后进栈执行
- 代码执行之前
- 初始化作用域链 scope-chain,当前自己的私有上下文,函数的作用域(创建函数时所在的上下文),链的右侧是当前上下文的上级上下文
- 初始化this
- 初始化arguments
- 形参赋值:在当前上下文中声明一个形参变量,并把传递的实参赋值给它
- 变量提升
- 代码执行
- 出栈释放
函数执行会形成一个私有的上下文,里面的变量受到上下文的保护, 不受外界干扰;
私有上下文有可能形成不被释放的上下文,里面的私有变量和值就会被保存起来,这些变量和值可以供下级上下文使用
我们把函数的这种保存和保护的机制称为闭包,简而言之即形成不被释放的上下文
需要注意的是,声明函数时创建的堆内存和执行函数时生成的函数执行上下文并不是同一个东西,有时候堆内存释放了但函数执行上下文还保留着。
[ 1 ]
1 | var x = [12,23]; |
[ 2 ]
1 | let x= 5; |
[ 3 ]
1 | let a = 0,b=0; |
[ 4 ]
1 | function fun(n,o){ |
垃圾回收
函数执行的时候需要进栈,如果有很多函数,浏览器就需要很多内存,这时候就需要有相应的机制回收执行完的函数
浏览器垃圾回收机制 / 内存(堆内存和栈内存)释放机制 GC
1 | 1. 栈内存的释放 |
箭头函数
箭头函数是ES6新增的一种写法,主要是为了简化函数的写法,下面展示一下普通函数在ES5中的写法和ES6中的写法的对比。
ES5:
1 | function func(x){ |
ES6:
1 | let func = x => y => x+y; |
如果函数参数只有一个值,那么包裹参数的小括号可以省略;并且如果函数体中只有一句return,则return也可以省略
一般箭头函数的语法:
1 | function func(x,y){ |
箭头函数和普通函数的区别:
- 箭头函数中没有arguments,而普通函数中,arguments是传入的参数形成的数组
- 箭头函数中this指向undefined,而普通函数中,this指向调用的对象或者是window
匿名函数
匿名函数一:向其他函数中传入函数
1 | setInterval(function(){ |
匿名函数二:自执行函数
1 | (function(i){ |
匿名函数具名化,就是给匿名函数设置一个名字,需要注意的是:
- 此时这个名字可以在当前函数形成的私有上下文中使用,代表当前函数本身
- 此名字不能再外部的上下文中使用
- 在本函数中使用时,它的值是不允许被修改的
- 如果当前的名字被上下文中的其他变量声明过,它的值是可以改动的,名字是私有变量
1 | (function b(){ |
构造函数
普通函数可以直接函数名+小括号的方式执行,其含义相当于在当前上下文执行一遍函数体中的语句,函数本身存在的目的也是为了代码的重用。而同样的,构造函数执行也会执行一遍函数体中的内容,不同的是,构造函数是new 函数名执行的,浏览器执行构造函数时会创建一个实例对象,且构造函数中this指向当前实例对象。
构造函数 VS 普通函数区别详解:
- 构造函数执行,也会像普通函数执行一样,形成私有的上下文
- 初始化SCOPE_CHAIN
- 形参赋值
- 变量提升
- 不同的地方:
- 创建上下文之后,浏览器默认会帮助我们创建一个“实例对象”;把Fn函数当作一个类“构造函数”, 创建的对象就是这个类的实例
- 初始this的时候,让this指向当前创建的实例对象
- 在代码执行完,返回值的时候,如果函数没有写return,或者返回的是基本数据类型,则浏览器默认会把创建的实例对象返回;如果函数本身返回的是一个引用数据类型,则返回引用数据类型
[ 1 ] 例
1 | function Fn(x, y) { |
生成器函数
生成器函数可以控制一个函数的暂停和继续,如下代码:
1 | function* fn() { |
- f.next() 会返回一个object对象,键值对为: { value: , done: } ,其中Value就是yield的返回值,而done表示迭代是否执行完,如果没有执行完,则为false
- f.next()可以接受一个参数,这个参数就是yield执行的返回值
- fn()生成器函数本身无法被new执行
f.__proto__
返回值是Generator的一个实例
[ 2 ] 例 基于生成器的斐波那契数列
1 | function *fibonacciSequence() { |
原型和原型链
对于其他后端语言来说,面向对象的实现、封装、继承、多态的实现,通常是采用类 ( Class ) 的方式,然而Javascript语言本身并不提供类实现,即使ES6中引入了Class,但它(Class)本身还是一个基于原型的语法糖。
当谈到继承时,JavaScript 只有一种结构:对象。每个实例对象( object )都有一个私有属性(称之为 proto )指向它的构造函数的原型对象(prototype )。该原型对象也有一个自己的原型对象( proto ) ,层层向上直到一个对象的原型对象为 null
。根据定义,null
没有原型,并作为这个原型链中的最后一个环节。
上面提到的构造函数,就是用于js继承的一种实现方式,构造函数的实例继承了构造函数原型上的方法,也可以调用原型上的方法(类似于公有方法)。举一个例子,Array数组有一系列的方法,slice,indexOf,filter等,这些方法都存在于数组这个原型上,实例通过原型链的方式进行继承。
1 | function Fn(){ |
类
类就是构造函数的语法糖
1 | class Person { |
如何用构造函数实现类?
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