原型、作用域、执行上下文、闭包

266 阅读9分钟

原型

构造函数:使用构造函数创建对象时调用的函数(也就是跟在new后面的函数),通常首字母大写。

prototype属性:仅存在于构造函数中,是构造函数身上的一个属性,该属性是一个对象,这个对象也就是所谓的原型对象,这个对象指向调用该构造函数创建的实例对象的原型(即xxx.__proto__的访问的这个对象,也就是所谓的是实例的原型对象)

⚠️ __proto__属性是可以访问到的,指向实例的原型对象,只是实例对象身上看不到该属性,也就是所谓的隐式原型

[[prototype]]:在主流浏览器中就是__proto__属性,它是无法直接通过实例.[[prototype]]去访问的,只能通过Object.getPrototypeOf来访问。

prototype和__proto__的区别: prototype是由构造函数创建的原型,是一个对象,也就是构造函数创建的实例的原型对象。__proto__是实例对象身上的一个属性,它指向的是实例的原型对象,是一个引向。

构造函数:

image.png

构造函数的prototype: image.png

实例(由图中可知,实例身上没有constructor属性):

image.png

实例的__proto__属性(指向实例的原型,所以图中的打印一摸一样,本质上指向的都是是堆中同一个对象)

image.png

对象的原型:Object.getPrototypeOf()  静态方法返回指定对象的原型(即内部 [[Prototype]] 属性的值

由上图可知:

1、实例的__proto__和构造函数的prototype属性指向是相同的,指向实例的原型对象:person.__proto__ === Person.prototype

2、constructor属性仅存于实例的原型对象身上,即Person.prototype.constructor === Person

3、所有的对象都是由new Object创建,即实例的原型对象是由new Object创建,由此得出实例的原型对象身上的__proto__属性指向Object的原型。

关系网:其中红色虚线就是原型链

image.png

思考: 为什么person.constrctor === Person是true?

由上述可知: constructor属性仅存在与实例的原型对象上,实例身上是没有这个属性的,所以根据原型链查找规则,去实例的原型对象身上找,Person.prototype身上是存在constructor这个属性的,因此返回true

继承: 每一个对象都会从原型链中继承属性,在js中对象是一个引用类型,所以继承并不是意味中赋值也不是复制了一份相同的代码,而是将指针复制了一份,两个指针指向堆中同一个对象

词法作用域and动态作用域

作用域

概念:定义变量的区间,能够确定变量的可访问范围

意义:确定了当前代码执行的时候哪些变量可以访问,是一个标准

分类:

静态作用域:在函数定义的时候就已经确定了,跟在哪里调用没有任何关系,一旦函数声明完成,可访问的范围立即确定。(在js中采用的是静态作用域,也就是词法作用域)

动态作用域:由函数调用时决定

// case 1
var scope = "global scope";
function checkscope(){
    var scope = "local scope";
    function f(){
        return scope;
    }
    return f();
}
checkscope();

// case 2
var scope = "global scope";
function checkscope(){
    var scope = "local scope";
    function f(){
        return scope;
    }
    return f;
}
checkscope()();

答案:local local

注:都是在checkscope函数内部创建的f函数,去访问scope会先从checkscop函数内部查找,若内部没有这个变量不存在,则回去全局作用域中查找。

作用域如图所示:

image.png

总结:作用域是在声明的时候决定的,this的指向是由调用方式决定的

执行上下文

概念:是一个抽象概念,可以理解为创造了一个环境,所有的js代码执行都必须在这个环境里面。

//变量声明提升
//首先返回的是一个变量,是一个函数表达式
var foo = function(){
  console.log("foo1")
}
foo()  //foo1
var foo = function(){
  console.log("foo2")
}
foo() //foo2

//函数声明提升
function foo(){
  console.log("foo1")
}
foo()  //foo2
function foo(){
  console.log("foo2")
}
foo() //foo2
console.log(add1(1,2)) //3
function add1(a,b){
  return a+b
}
console.log(add2(1,3)) //报错:add2 is not a function
//变量声明提升:只提升变量,函数留在当前位置,直到js执行到这一行才开始进行赋值操作
var add2 = function(a,b){
  return a+b
}
//思考:此时两个console.log有什么区别?
//知识点:函数声明提升,当我们使用函数声明一个时,会将函数放到当前执行代码的前面,当使用变量声明函数时候,会出现变量声明提升,此时var add 2 = undefined,此时undefined不是一个函数出现上述错误

栈(stack):是真实物理空间上的一个栈,这个栈就以为着是一个执行上下文的栈(execution context stack 简称 ECS),用来管理执行上下文的。

栈的特点:先进后出

执行过程:当进入到js代码的时候时候会先创建一个全局上下文栈,在函数执行的时候再创建一个函数执行上下文栈,这个栈的底部永远存放的的全局上下文,所有的函数全部执行完,全局上下文栈出栈。

image.png 如何判断上下文何时出栈: 看返回的是函数的调用还是函数,若内部仍存在函数调用,则需要等到内部所有函数调用完成之后才能进行出栈操作,若内部返回的时候变量或者函数,则立马出栈。

如图说明有什么区别? image.png 区别

  1. checkscope():先全局执行上下文进栈,后执行 checkscope()执行上下文进栈,内部出现 return fn()函数执行,fn()执行上下文进栈道,出栈道顺序:fn()-->checkscope()-->全局执行栈
  2. checkscope()():先全局执行上下文进栈,后执行checkscope()执行上下文进栈,只是返回了一个变量,内部没有其他函数依赖checkscope执行环境,checkscope()出栈,之后fn()开始进栈道。出栈顺序:checkscope()-->fn() -->全局执行栈

思考:为什么checkscope()的执行上下文都已经销毁了,执行fn()得到的scope的答案已经是local scope而不是global scope?

在创建一段可执行代码时会做什么是事情呢

三大属性:

1、变量对象

概念: 创建变量的一个作用域,存放执行上下文中声明的一些变量或者函数

分类:

全局变量对象(VO):js提前预制好的变量对象(例如:parseInt、Math.ceil等方法),可以使用window直接去使用全局变量

console.log(Math.random())
console.log(this) //在global环境中this默认指向作用域链的最顶端,也就是window

函数上下文的变量对象(AO):函数调用的时候才会执行,也就是激活的时候创建的对象(active Objct),简称AO,对应活动态的一个对象,也就是活动对象。

AO和VO的区别:AO是存放函数执行上下文的变量声明、函数声明以及arguments,而VO是放全局执行上下文的变量声明,函数声明以及argumengts

当进入执行上下文中,此时还没执行函数,在准备中此时js引擎针对变量对象做了三件事: (1)函数声明 (function(){} 这也是为什么我们在函数声明前执行函数是ok的原因,在执行前就已经将函数进行了声明) (2)变量声明(只会去声明变量,赋值是在代码执行中去做,此时值为undefined) (3)声明arguments

image.png

代码执行之后(代码执行是按照顺序执行的):此时依次开始赋值 image.png

误区知识点: 声明提升

  1. 函数变量提升:使用函数声明的函数才具有声明提升,使用函数表达式的函数不具备此功能
  2. 变量提升:使用var声明的变量会将变量提升到作用域的最顶端
console.log("foo",foo) //输出foo函数
console.log("foo",foo) //输出foo函数
//原因:函数表达式不具备声明提升,所以访问foo的时候都访问的都是foo函数
function foo() { console.log('foo1'); } 
var foo = function() { console.log('foo2'); } 
console.log("foo",foo) //输出foo2函数
console.log("foo",foo) //输出foo2函数
//原因:当变量和函数名相同时候,会发生覆盖问题,谁在后面就输出谁

注意:在函数内部没有使用var、let、const去声明的变量,此时意味着这个变量是不是存储到函数上下文中,在函数执行的时候,才将他在全局执行上下文中声明,在声明前访问都是无法找到这个变量的

     function foo() {
        console.log('a', a);
        a = 3;
      }
      foo(); //报错,无法找到a
      //   AO = {
      //     argumengs: {
      //       length: 0
      //     }
      //   };

      function bar() {
        a = 1;
        console.log('a', a);
      }
      bar(); //1
      //   AO = {
      //     argumengs: {
      //       length: 0
      //     }
      //   };
      //   VO = {
      //     a: 1
      //   };

2、作用域链

概念:查找变量的规则,查找变量时,首先会从当前的执行上下文中查找,若在当前上下文中未找到会去父级的执行上下文中去查找,按照层级一直往上找,直到全局上下文也就是window对象,若还未找到,则该变量不存在。

var scope = "global scope";
function checkscope(){
    var scope2 = 'local scope';
    return scope2;
}
checkscope();

创建到执行:

1、首先创建函数,保存作用域链到内部属性 [[scope]] (通过这个属性来表达作用域的意思)

checkscope.[[scope]] =[
  //最外层的作用域一定是全局上下文变量对象
  globalContext.VO
]

2、执行函数,创建函数执行上下文,并且将执行上下文压入执行上下文栈中

ECStack = [
    checkscopeContext,
    globalContext
];

3、准备执行上下文的三要素,

(1)保存作用域链,需要创建将作用域链赋给上下文的scope属性

checkscopeContext={
  scope:checkscope.[[scope]]
}

(2)初始化活动对象

AO={
  argments:{
  length:0
  },
  scope2:undefined,
  scope:checkscope.[[scope]]
}

4、将活动对象压入checkscope作用域链的最顶端

checkscopeContext = {
    AO: {
        arguments: {
            length: 0
        },
        scope2: undefined
    },
    Scope: [AO, [[Scope]]]
}

5、执行函数,修改AO中的属性值

AO={
  argments:{
>   length:0
  },
  scope2:'local scope',
  scope:[AO,[[scope]]]
}

6、查找到scope2的值,函数执行完毕,函数上下文从执行上下文栈中弹出,函数上下文环境销毁。

3、this

核心:谁调用的当前这个值,则this就指向的是谁

闭包

MDN:能够访问自由变量的函数

自由变量:即不是函数的参数,也不是函数局部变量的变量(不是函数内部的值,但是在函数内部能够使用)

var a = 1
function foo(){
  console.log(a)
}
foo() //1

本质:获取到作用域链中上一层的变量对象(切记js中是静态作用域!!!!!)

var scope = "global scope";
function checkscope(){
    var scope = "local scope";
    function f(){
        return scope;
    }
    return f;
}
var foo = checkscope();
foo();