#js函数的深入剖析 #
函数是编程中一个重要的概念,它可以帮助我们用更简洁更有可读性的代码完成功能。而语言的设计者们也总是喜欢把一门语言的语法糖埋在函数中,等待我们这些语言的使用者探索和品尝
下面我们就来讨论一下,什么是函数?什么是作用域?什么是函数的执行上文?什么是预解析。这些语法有什么作用?是如何实现的?我们要怎么利用这些特性?
- 函数的基本知识
1.什么是函数
* 实现特定功能的n条语句的封装体,是可以执行的对象
2.函数的作用
* 代码服用
* 增强可读性
3.回调函数
- 什么函数才是回调函数?
1). 你定义的
2). 你没有调用
3). 但最终它执行了(在某个时刻或某个条件下)
- 常见的回调函数?
* dom事件回调函数 ==>this:发生事件的dom元素
* 定时器回调函数 ===>this:window
* ajax请求回调函数
* 生命周期回调函数
4 自调用函数 /IIFE / Immediately-Invoked Function Expression / 立即执行函数
作用:
* 隐藏实现
* 不会污染外部(全局)命名空间
* 用它来编码js模块
- 原型和原型链
1.原型最基本的三个概念
-
任何函数fun都有一个prototype属性,指向一个对象,这个对象就是函数fun的显式原型
-
任何对象obj都有一个【proto】属性,指向一个对象,这个对象就是对象obj的隐式原型
-
对象的隐式原型等于它构造函数的显式原型,(任何函数都有显式原型,但只有函数做构造式prototype属性才有意义)
function Fn () { } console.log(Fn.prototype) //函数显式原型中有一个constructor属性指向函数本身 console.log(Fn.prototype.constructor===Fn) // //为Fn的显式原型添加方法 Fn.prototype.test = function () { console.log('test()') } var fn = new Fn() //Fn的实例fn的隐式原型等于Fn的显式原型 fn.test()
2.原型基本用途:在构造函数中保存其实例对象的公共方法
-设计思路: 为什么方法通常要存放在构造函数原型内?,节省内存空间,如果在构造函数中用this.xxx=function(){}的方式定义方法,那每构造一个实例都会专门开辟一片内存保存这些重复的方法。
3深入一些
1.函数显式原型在函数定义是被创建,其本质是一个Object的实例,所以函数的显式原型也存在隐式原型,且默认等于Object的显式原型
allFun.prototype.__proto__ === Object.prototype //true,allFun指代所有函数
2.Object也是函数但它的显式原型是个特例,Object的显式原型不是任何构造函数的实例
` Object.prototype.__proto__ === null //true`
3.任何函数都是Function的实例,包括Object和Function本身
allFun.prototype. === Function.prototype
Function.__proto__ === Function.prototype
Object.__proto__ === Function.prototype
4.原型链
-
访问一个对象的属性时
- 先在自身属性中查找,找到返回
- 如果没有, 再沿着__proto__这条链向上查找, 找到返回
- 如果最终没找到, 返回undefined
-
原型链是实例对象的概念,也就是隐式原型链
-
给对象属性赋值时不涉及原型链,只有在读属性/方法时才会沿原型链查找
-典型案例
//案例一: function fun(){}; var a = new fun(); fun.prototype.prop = 1; console.log(a.prop);//1 //此时a.__proto__ === fun.prototype; //案例二: function fun(){}; fun.prototype.prop = 1; var a = new fun(); fun.prototype={a:2}; console.log(a.prop);//1 //此时a.__proto__和fun.prototype不是一个对象,a.__proto__还是指向原来的对象 //案例三: function F (){} Object.prototype.a = function(){ console.log('a()') } Function.prototype.b = function(){ console.log('b()') } var f = new F() f.a()//执行 F.a()//执行 F.b()//执行 f.b()//f.b is not a function
函数执行–执行上下文/执行上下文栈/预解析
1.函数执行上下文–函数执行时js引擎到底干了什么?
基本概念:执行上下文可以理解成代码执行的环境,在具体代码执行之前,js引擎就会搭建这样一个环境
全局执行上下文和函数执行上下文:js代码可分成全局代码和函数代码,为什么代码执行搭建的环境就叫做什么上下文
全局执行上下文:window
全局代码执行前:
- 在执行全局代码前将window确定为全局执行上下文
- 对全局数据进行预处理
- var定义的全局变量==>undefined, 添加为window的属性
- function声明的全局函数==>赋值(fun), 添加为window的方法
- this==>赋值(window)
- 开始执行全局代码
函数执行上下文:
在调用函数, 准备执行函数体之前, 创建对应的函数执行上下文对象(虚拟的, 存在于栈中),执行上下文本身是语言特性,但浏览器实现时将器现成对象的结构
对局部数据进行预处理
- 参形变量==>赋值(实参)==>添加为执行上下文的属性
- arguments==>赋值(实参列表), 添加为执行上下文的属性
- var定义的局部变量==>undefined, 添加为执行上下文的属性
- function声明的函数 ==>赋值(fun), 添加为执行上下文的方法
- this==>赋值(调用函数的对象)
- 开始执行函数体代码
函数执行上下文对象不是一个具体的对象而是一个环境,为函数中代码执行做好准备
执行上下文栈
一个存储函数执行上下文的栈结构的容器,其特点是后进先出。
1. 在全局代码执行前, JS引擎就会创建一个栈来存储管理所有的执行上下文对象
2. 在全局执行上下文(window)确定后, 将其添加到栈中(压栈)
3. 在函数执行上下文创建后, 将其添加到栈中(压栈)
4. 在当前函数执行完后,将栈顶的对象移除(出栈)
5. 当所有的代码执行完后, 栈中只剩下window
变量提升和函数提升
变量提升和函数提升是js引擎创建执行上下文时进行的一项重要工作
- 变量声明提升
- 通过var定义(声明)的变量, 在定义语句之前就可以访问到
- 值: undefined
- 函数声明提升
-
通过function声明的函数, 在之前就可以直接调用
-
值: 函数定义(对象)
典型案列
案例一: function a() {} var a console.log(typeof a)//"function" // 函数提升优先于变量提升(函数后提升) 案例二: if (!(b in window)) { var b = 1 } console.log(b) //undefine; //在if判断体中的变量也能提升 if (!(b in window)) { var b = 1 } console.log(b) //undefine; 案例三: fun(); if(true){ function fun(){console.log('fun')}; } //会报错,fun is not a function *案例四,很经典* var a=1; function a(a){console.log(a);var a =2 }; a(a); //会报错,a is not a function //解释 var a; 和a的函数定义都被预处理,之后代码执行的顺序是 a = 1; a(a);
作用域和作用域链
理解:作用域是一块地盘,是代码所在的区域,这块区域是在代码编写时就存在的,与代码的执行无关。它也是一个语言特性,被浏览器具现成一个对象
作用域分类:全局作用域,函数作用域(es6之前)
作用域的作用:隔离变量,不同作用域下同名变量不会有冲突;
作用域和执行上下文的区别:
作用域在代码编写时就存在,执行上下文在代码执行前被创建。
编写里n个函数就有n+1个作用域,执行n个函数,就有n+1执行上下文。
作用域是静态的, 只要函数定义好了就一直存在, 且不会再变化
执行上下文是动态的, 调用函数时创建, 函数调用结束时就会自动释放
作用域和执行上下文的联系
执行上下文从属于对应的作用域,也可以说执行上下文时依凭着作用域被创建的,或者说执行上下文时作用域在代码执行时的实现。
为什么作用域是代码编写是就存在而不是函数被预解析时存在?
首先,函数被定义有两个概念,一个是我们编写一个函数,再就是函数被js引擎预处理。
为了区分这两个概念,我们在当前环境不使用“函数定义”的概念,而是进行具体描述
每个函数都有自己的作用域来保证自身变量不会被其他作用域的同名变量影响,也要保证不影响其他作用域的同名变量
假设作用域需要函数被解析才存在,那有些内部函数,可能没有预解析的机会
但他们的依然存在不影响外界也不被外界影响的独立命名空间。
作用域链
作用域发生嵌套,全局作用域会嵌套其他作用域,外层函数作用域会嵌套内层函数作用域
- 理解
- 多个上下级关系的作用域形成的链, 它的方向是从下向上的(从内到外)
- 查找变量时就是沿着作用域链来查找
- 查找一个变量的查找规则
- 在当前作用域下的执行上下文中查找对应的属性, 如果有直接返回, 否则进入2
- 在上一级作用域的执行上下文中查找对应的属性, 如果有直接返回, 否则进入3
- 再次执行2的相同操作, 直到全局作用域, 如果还找不到就抛出找不到的异常
典型案例
案例一:
var x = 10;
function fn() {
console.log(x);
}
function show(f) {
var x = 20;
f();
}
show(fn);//10
查找变量要顺着作用域链查找,作用域链的嵌套关系只和函数编写的位置有关,域调用的位置无关。
案例二:
var fn = function () {
console.log(fn)
}
fn() // fn函数
//可以顺着最作用域链找到对应它本身的变量
案例三:
var obj = {
fn2: function () {
// console.log(fn2) // 报错
console.log(fn2)
}
}
obj.fn2()//报错,fn2 is definded
作用域链上不存在fun2,但存在obj,所以可以写成 console.log(obj.fn2)
浏览器语言特性的实现者
其实无论作用域还是执行上下文都是js的语言特性,这些语言特性是为开发者服务的,使得语言的使用者更方便的组织代码。而且对javascript来说这些特性的实现都需要浏览器的配合。
就拿chrome来说,在代码执行前,浏览器创建一个了Scope对象,它对应一块内存空间,也就是我们常说的栈内存(执行栈结构)。Scope“底部”始终有一个Globle属性,它指向一个对象,这个对象对应着全局执行上下文。函数执行时,Scope中“上部”会生成一个Loble属性,它指向的对象对应该函数的执行上下文。
值得注意的是,这些对象都是隐式对象,具体说就是它们是“浏览器的对象”而不是Window的对象。
js函数中还有一个很重要的概念闭包,处于篇幅,和这个重要语法糖的"尊重",下来我们单独论述