本文主要梳理JS 函数执行的整个过程,包括执行上下文,作用域链,内存空间,闭包,this指向和call,apply,bind等,会持续补充更新哦!
评估和执行 JavaScript 代码的环境的抽象概念。
全局执行上下文— 这是默认或者说基础的上下文,任何不在函数内部的代码都在全局上下文中(一个程序中只会有一个全局执行上下文)。
函数执行上下文— 每当一个函数被 调用 时, 都会为该函数创建一个新的上下文(函数上下文可以有任意多个)。每当一个新的执行上下文被创建,它会按定义的顺序执行一系列步骤。
Eval 函数执行上下文— 执行在 eval 函数内部的代码也会有它属于自己的执行上下文,但由于 JavaScript 开发者并不经常使用 eval,暂不讨论。
全局上下文
生成变量对象:全局对象(浏览器的情况下为window )
建立作用域链:全局对象
确定this指向:设置 this 的值等于全局对象(var === this. === winodw.)。
函数上下文
生成变量对象:用活动对象(activation object, AO)来表示变量对象(活动对象是在进入函数上下文时刻被创建的,它通过函数的 arguments 属性初始化)。
函数有一个内部属性 [[scope]],当函数创建的时候,就会保存所有父变量对象到其中。
当函数激活时,进入函数上下文,创建 VO/AO后,就会将活动对象添加到作用链的前端。
Scope (作用域链)= [AO].concat([[Scope]]);
确定this指向:this 永远指向最后 调用 它的那个对象(参见后文)
进入执行上下文
这时候还没有执行代码, 变量对象 会加入:
由 名称和对应值 组成的一个变量对象的属性被创建
没有 实参 ,属性值设为** undefined**
由名称和对应值(函数对象(function-object))组成一个变量对象的属性被创建
如果变量对象已经存在 相同名称 的属性,则 完全替换 这个属性
由**名称和对应值(undefined)**组成一个变量对象的属性被创建(var);
如果 变量名称 跟已经声明的 形参 或 函数 相同,则变量声明不会干扰已经存在的这类属性
变量声明提升:可以在声明之前访问 var 定义的变量(虽然是 undefined),但是在声明之前访问 let 和 const 的变量会得到一个引用错误(未初始化)。
代码执行
顺序执行代码,根据代码,修改变量对象的值
var scope = "global scope";
function checkscope(){
var scope2 = 'local scope';
return scope2;
}
checkscope();
复制代码
执行过程如下:
checkscope 函数被创建,保存作用域链到 内部属性[[scope]]
执行 checkscope 函数,创建 checkscope 函数执行上下文,checkscope 函数执行上下文被压入执行上下文栈
checkscope 函数并不立刻执行,开始做准备工作,第一步:复制函数[[scope]]属性创建作用域链
第二步:用 arguments 创建活动对象,随后初始化活动对象,加入形参、函数声明、变量声明
第三步:将活动对象压入 checkscope 作用域链顶端
准备工作做完,开始执行函数,随着函数的执行,修改 AO 的属性值
查找到 scope2 的值,返回后函数执行完毕,函数上下文从执行上下文栈中弹出
存储代码运行时创建的所有执行上下文。
当 JavaScript 引擎第一次遇到你的脚本时,它会创建一个全局的执行上下文并且压入当前执行栈。每当引擎遇到一个函数调用,它会为该函数创建一个新的执行上下文并压入栈的顶部。
引擎会执行那些执行上下文位于栈顶的函数。当该函数执行结束时,执行上下文从栈中弹出,控制流程到达当前栈中的下一个上下文。
作用域是指程序源代码中定义变量的区域。
作用域规定了如何查找变量,也就是确定当前执行代码对变量的访问权限。
JavaScript 采用词法作用域(lexical scoping),也就是静态作用域。
词法作用域, 函数的作用域在函数定义的时候就决定了 。
动态作用域,函数的作用域是在函数调用的时候才决定的。
当查找变量的时候,会先从当前上下文的变量对象中查找。
如果没有找到,就会从父级(词法层面上的父级)执行上下文的变量对象中查找,一直找到全局上下文的变量对象,也就是全局对象。
这样由多个执行上下文的变量对象构成的链表就叫做作用域链。
作用域链和原型继承查找时的区别:如果去查找一个普通对象的属性,但是在当前对象和其原型中都找不到时,会返回 undefined ;但查找的属性在作用域链中不存在的话就会抛出 ReferenceError 。
JS内存空间分为 栈(stack) 、 堆(heap) 、 池(一般也会归类为栈中) 。 其中 栈 存放变量, 堆 存放复杂对象, 池 存放常量,所以也叫常量池。
分配你所需要的内存
使用分配到的内存(读、写)
不需要时将其释放、归还
JS有自动垃圾收集机制,常用 标记清除 算法来找到哪些对象是不再继续使用的,当将变量设为null时释放引用,脱离执行环境,这个值会在下一次垃圾收集器执行操作时被找到并释放。
局部变量:局部作用域中,函数执行完毕,局部变量没有存在意义,垃圾收集器很容易做出判断并回收。
全局变量:全局变量什么时候需要自动释放内存空间则很难判断,所以在开发中尽量 避免 使用全局变量。
常见垃圾回收算法
看一个对象是否有指向它的 引用 。如果没有其他对象指向它了,说明该对象已经不再需要了。
循环引用:如果两个对象相互引用,尽管他们已不再使用,但是垃圾回收器不会进行回收,最终可能会导致内存泄露。
从根部(在JS中就是全局对象)出发定时扫描内存中的对象,凡是能从根部到达的对象, 保留 。那些从根部出发无法触及到的对象被标记为 不再使用 ,稍后进行回收。
无法触及的对象包含了没有引用的对象这个概念,但反之未必成立。
1、意外的全局变量
function foo(arg) {
a = "this is a hidden global variable"; //未使用var定义
this.b = "potential accidental global"; //this指向全局
}
复制代码
解决方法:
在 JavaScript 文件头部加上 'use strict'
,使用严格模式避免意外的全局变量,此时 上例中的this指向 undefined
。如果必须使用全局变量存储大量数据时,确保用完以后把它设置为 null 或者重新定义。
2、被遗忘的计时器或回调函数
必须手动终止定时器
现代的浏览器(包括 IE 和 Microsoft Edge)使用了更先进的垃圾回收算法(标记清除),已经可以正确检测和处理循环引用了。即回收节点内存时,不必非要调用 removeEventListener 了。
3、脱离 DOM 的引用
如果把DOM 存成字典(JSON 键值对)或者数组,此时,同样的 DOM 元素存在两个引用:一个在 DOM 树中,另一个在字典中。那么将来需要把两个引用都清除。
闭包是一个可以访问外部作用域的内部函数,即使这个外部作用域已经执行结束。闭包的外部作用域是在其 定义 的时候已决定,而不是执行的时候。
闭包中的变量并不保存中栈内存中,而是保存在堆内存中,这也就解释了函数之后之后为什么闭包还能引用到函数内的变量,这些被引用的变量直到闭包被销毁时才会被销毁。
闭包使得 timer 定时器,事件处理,AJAX 请求等异步任务更加容易,可以通过闭包来达到封装性。
能不能访问关键看在哪里定义,而不是在哪里调用, 调用方法的时候,会跳转到定义方法时候的环境里,而不是调用方法的那一行代码所在的环境。
闭包引起的内存泄露那都是因为浏览器的gc问题(IE8以下为首)导致的,跟js本身没有关系,所以,请不要再问js闭包会不会引发内存泄露了
闭包只存储外部变量的引用,而不会拷贝这些外部变量的值。var 只有函数作用域 let,coast有函数作用域和块作用域。
问一个问题
下面的两段代码中, checkscope() 执行完成后,闭包 f 所引用的自由变量 scope 会被垃圾回收吗?为什么?
var scope = "global scope";
function checkscope(){
var scope = "local scope";
function f(){
return scope;
}
return f;
}
checkscope()();
复制代码
var scope = "global scope";
function checkscope(){
var scope = "local scope";
function f(){
return scope;
}
return f;
}
var foo = checkscope();
foo();
复制代码
第一段中自由变量特定时间之后回收:执行完毕后出栈,该对象没有绑定给谁,从Root开始查找无法可达,此活动对象一段时间后会被回收 第二段中自由变量不回收:此对象赋值给 var foo = checkscope(); ,将foo压入栈中,foo指向堆中的f活动对象,对于Root来说可达,不会被回收。
如果想让第二段中自由变量回收,要怎么办?
foo = null;,把引用断开就可以了。
this 永远指向 最后调用它的那个对象
this的值不会被保存在作用域链中,this的值取决于函数被调用的时候的情景(也就是执行上下文被创建时确定的)。
new绑定:作为一个构造函数,this绑定到 新创建的对象 ,注意:显示return函数或对象,返回值不是新创建的对象,而是显式返回的函数或对象。
call()、apply()--this指向绑定的对象上
bind()--this将永久地被绑定到了bind的第一个参数
隐式绑定:this指向**调用函数的对象,**由上下文对象调用时,绑定到上下文对象
默认绑定: 非严格模式情况下,this 指向 window(全局变量), 严格模式下,this指向 undefined
箭头函数不绑定this,箭头函数中的this相当于普通变量。
箭头函数的this寻值行为与普通变量相同,在作用域中逐级寻找。
箭头函数的this无法通过bind,call,apply来 直接 修改(可以间接修改)。
改变作用域中this的指向可以改变箭头函数的this。
eg. function closure(){()=>{//code }}
,在此例中,我们通过改变封包环境 closure.bind(another)()
,来改变箭头函数this的指向
作为一个DOM事件处理函数--this指向触发事件的元素,也就是始事件处理程序所绑定到的DOM节点。
立即执行函数(function() {})()中的this指向的window对象,因为完整写法就是window.(function() {})()
new 绑定 > 显式绑定 > 隐式绑定 > 默认绑定
三者都是用来改变函数的this指向
三者的第一个参数都是this指向的对象
bind是返回一个绑定函数可稍后执行,call、apply是立即调用
三者都可以给定参数传递
call和bind给定参数需要将参数全部列出,apply给定参数数组
//ES6实现
Function.prototype.myCall=function(context){
context=context || window //当参数为null时指向window
var args=[...arguments].slice(1)//将类数组对象转为数组并截取从1到结尾的参数
var fn = Symbol() //设定fn为唯一属性
context[fn]=this //fn绑定当前函数
var result=context[fn](...args) //传入参并执行函数,考虑有返回值的情况
delete context[fn] //删除fn
return result //返回return值
}
//ES3实现
Function.prototype.myCall=function(context){
context=context || window
var args=[]
for(var i=1;i
apply和call的区别是call需要列出所有参数,而apply传入一个参数数组
//ES6实现
Function.prototype.myApply=function(context){
context=context || window
var args=arguments[1]||[] //与call不同的地方是直接传入一个参数数组,获取该数组
var fn = Symbol();
context[fn]=this
var result=context[fn](...args)
delete context[fn]
return result
}
//ES3实现
Function.prototype.myApply=function(context){
context=context || window
var args=arguments[1]||[] //与call不同的地方是直接传入一个参数数组,获取该数组
context.fn=this
var result=eval('context.fn('+args+')')
delete context.fn
return result
}
复制代码
1、可以指定 this
2、返回一个函数
3、可以传入参数
4、柯里化
Function.prototype.myBind = function (context) {
if (typeof this !== "function") {
throw new Error("Function.prototype.bind - what is trying to be bound is not callable");
}
var self = this
var args1 = [...arguments].slice(1)
var fn = function () { };
var newContext = function () {
var args2 = [...arguments]
return self.myCall(this instanceof fn ? this : context, ...args1, ...args2);
}
if (this.prototype) {
fn.prototype = this.prototype
}
newContext.prototype = new fn();
return newContext;
}
复制代码
var scope = "global scope";
function checkscope(){
var scope = "local scope";
function f(){
return scope;
}
return f();
}
checkscope();
复制代码
var scope = "global scope";
function checkscope(){
var scope = "local scope";
function f(){
return scope;
}
return f;
}
checkscope()();
复制代码
两段代码都会打印: local scope
。 JavaScript采用的是词法作用域,函数的作用域基于函数 创建 的位置。
var scope = "global scope";
function checkscope(){
var scope = "local scope";
function f(){
return scope;
}
return f();
}
checkscope();
复制代码
var scope = "global scope";
function checkscope(){
var scope = "local scope";
function f(){
return scope;
}
return f;
}
checkscope()();
复制代码
执行上下文栈的变化不一样
//模拟第一段代码:
ECStack.push( functionContext);
ECStack.push( functionContext);
ECStack.pop();
ECStack.pop();
//模拟第二段代码:
ECStack.push( functionContext);
ECStack.pop();
ECStack.push( functionContext);
ECStack.pop();
复制代码
function foo() {
console.log(a);
a = 1;
}
foo();
复制代码
function bar() {
a = 1;
console.log(a);
}
bar();
复制代码
第一段会报错: Uncaught ReferenceError: a is not defined
( "a" 并没有通过 var 关键字声明,所有不会被存放在 AO 中) 第二段会打印: 1
(执行 console 的时候,全局对象已经被赋予了 a 属性)
console.log(foo);
function foo(){
console.log("foo");
}
var foo = 1;
复制代码
会打印函数,而不是 undefined (如果 变量名称 跟已经声明的 形参 或 函数 相同,则变量声明 不会干扰 已经存在的这类属性)
var foo = function () {
console.log('foo1');
}
foo();
var foo = function () {
console.log('foo2');
}
foo();
复制代码
function foo() {
console.log('foo1');
}
foo();
function foo() {
console.log('foo2');
}
foo();
复制代码
第一段会打印:foo1 和 foo2 变量声明提升 (提升为undefined,边执行边赋值) 第二段会打印:foo2 和 foo2 函数声明提升 (函数提升,当重复时后一个会对前一个进行覆盖)
var a = {n: 1};
var b = a;
a.x = a = {n: 2};
console.log(a.x)
console.log(b.x)
复制代码
a.x:undefined b.x:{n: 2} 原因:
. = a.x {n: 1} {n: 1, x: undefined} b.x
2、赋值操作是 从右到左
,所以先执行 a = {n: 2}
, a
的引用就被改变了,然后这个返回值又赋值给了 a.x
, 需要注意 的是这时候 a.x
是第一步中的 {n: 1, x: undefined}
那个对象,其实就是 b.x
,相当于 b.x = {n: 2}
var person = {
name: "personName",
getName: function(){
return this.name;
}
}
console.log(person.getName());
复制代码
var name = "windowName";
var person = {
name: "axuebin",
getName: function(){
return this.name;
}
}
var getName = person.getName;
console.log(getName());