kangkai124 / blog

开发笔记
https://kangkai124.github.io/blog/
MIT License
26 stars 4 forks source link

JavaScript深入尝试 #23

Open kangkai124 opened 5 years ago

kangkai124 commented 5 years ago

原型链,继承,作用域,上下文,this,闭包,传参,call,apply,bind,new。。

主要参考:https://github.com/mqyqingfeng/Blog/issues/17 和 《你不知道的JavaScript》

kangkai124 commented 5 years ago

原型和原型链

image

function Person () {}
var person = new Person()
person.__proto__ === Person.prototype
Object.getPrototypeOf(person) === Person.prototype
Person === Person.prototype.constructor
Person.prototype.__proto__ === Object.prototype
Object.getPrototypeOf(Person.prototype) === Object.prototype
Object.prototype.__proto__ === null
kangkai124 commented 5 years ago

作用域

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()();

上面两端代码执行结果都是 local scope

JavaScript 函数的执行用到了作用域链,这个作用域链是在函数定义的时候创建的。嵌套的函数 f() 定义在这个作用域链里,其中的变量 scope 一定是局部变量,不管何时何地执行函数 f(),这种绑定在执行 f() 时依然有效。

kangkai124 commented 5 years ago

上面两段代码执行的结果一样,但是两段代码究竟有哪些不同呢?

答案就是执行上下文栈的变化不一样。

让我们模拟第一段代码:

ECStack.push(<checkscope> functionContext);
ECStack.push(<f> functionContext);
ECStack.pop();
ECStack.pop();

让我们模拟第二段代码:

ECStack.push(<checkscope> functionContext);
ECStack.pop();
ECStack.push(<f> functionContext);
ECStack.pop();
kangkai124 commented 5 years ago

执行上下文

当 JavaScript 执行一段可执行代码时,会创建对应的执行上下文(执行环境),执行上下文可以抽象的理解为一个 object,由以下几个属性构成:

kangkai124 commented 5 years ago

变量对象 VO

每一个执行上下文都会分配一个 变量对象(variable object) ,变量对象的属性由 变量(variable)函数声明(function declaration) 构成,变量对象保存了当前作用域的所有函数和变量。

活动对象 AO

在函数上下文中,我们用活动对象(activation object, AO)来表示变量对象。

当函数被激活,那么一个活动对象就会被创建并且分配给执行上下文。活动对象由特殊对象 arguments 初始化而成。随后,他被当做变量对象用于变量初始化。

变量对象的创建过程

  1. 全局上下文的变量对象初始化是全局对象

  2. 函数上下文的变量对象初始化只包括 Arguments 对象

  3. 在进入执行上下文时会给变量对象添加形参、函数声明、变量声明等初始的属性值

  4. 在代码执行阶段,会再次修改变量对象的属性值

function a(name, age){
    var gender = "male";
    function b(){}
}
a(“k”,10);

a 被调用时,在 a 的执行上下文会创建一个活动对象 AO,并且被初始化为 AO = [arguments],随后AO 又被当做变量对象 VO 进行变量初始化,此时 VO = [arguments].concat([name,age,gender,b])。

kangkai124 commented 5 years ago

作用域链

当查找变量的时候,会先从当前上下文的变量对象中查找,如果没有找到,就会从父级(词法层面上的父级)执行上下文的变量对象中查找,一直找到全局上下文的变量对象,也就是全局对象。这样由多个执行上下文的变量对象构成的链表就叫做作用域链

以下面的例子为例,结合着之前讲的变量对象和执行上下文栈,我们来总结一下函数执行上下文中作用域链和变量对象的创建过程:

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

执行过程如下:

1.checkscope 函数被创建,保存作用域链到 内部属性[[scope]]

checkscope.[[scope]] = [
    globalContext.VO
];

2.执行 checkscope 函数,创建 checkscope 函数执行上下文,checkscope 函数执行上下文被压入执行上下文栈

ECStack = [
    checkscopeContext,
    globalContext
];

3.checkscope 函数并不立刻执行,开始做准备工作,第一步:复制函数[[scope]]属性创建作用域链

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

4.第二步:用 arguments 创建活动对象,随后初始化活动对象,加入形参、函数声明、变量声明

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

5.第三步:将活动对象压入 checkscope 作用域链顶端

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

6.准备工作做完,开始执行函数,随着函数的执行,修改 AO 的属性值

checkscopeContext = {
    AO: {
        arguments: {
            length: 0
        },
        scope2: 'local scope'
    },
    Scope: [AO, [[Scope]]]
}

7.查找到 scope2 的值,返回后函数执行完毕,函数上下文从执行上下文栈中弹出

ECStack = [
    globalContext
];