深入JavaScript(1)原型和原型链、词法作用域和动态作用域、执行上下文

95 阅读6分钟

1.原型&原型链

1.1prototype

每个函数都有一个prototype属性

prototype是函数才会有的属性,__proto__是每个对象都有的属性

function Father() {
    
}
Father.prototype.name = 'along';
var son1 = new Father();
console.log(son1.name) //along

函数的protype属性指向一个对象,这个对象正是调用该构造函数而创建的实例的原型,也就是例子中:Father函数的prototype指向son1的原型。

那么什么是原型呢?可以这样理解:每一个JavaScript对象(null除外)在创建的时候就会与之关联另一个对象,这个对象就是我们所说的原型(__ proto__),每一个对象都会从原型“继承”属性。

1.2__proto__

这是每一个JavaScript对象(除了null)都具有的一个属性,叫__proto__,这个属性指向该对象的原型。

function Father () {

}
let son = new Father();

console.log(son.__proto__ === Father.prototype); //true

image.png

1.3constructor

实例对象和构造函数都可以指向原型,那么原型是否有属性指向构造函数或者实例呢?

指向实例倒是没有,因为一个构造函数可以生成多个实例,但是原型指向构造函数是有的:constructor,每个原型都有一个constructor属性指向关联的构造函数

function Father(){ }
console.log(Father.prototype.constructor === Father); //true

image.png

function Father() {}
let son = new Father();

console.log(son.__proto__ === Father.prototype); //true
console.log(Father.prototype.constructor === Father); //true
// Object.getPrototypeOf() 方法返回指定对象的原型
console.log(Object.getPrototypeOf(son) === Father.prototype); //true

1.4实例与原型,原型的原型,原型链

读取实例的属性时,如果找不到,就会查找与对象关联的原型中的属性,如果还查不到,就去原型的原型,一直找到最顶层为止。

function Father() {}
let son = new Father();

Father.prototype.name = '我是爸爸'
son.name = '我是儿子'

console.log(son.name); //'我是儿子'
delete son.name

console.log(son.name); //'我是爸爸'

那么原型的原型又是什么呢?其实原型对象就是通过Object 构造函数生成的,结合之前所讲,实例的 __ proto __ 指向构造函数的prototype。

那Object.prototype的原型呢? null!

null表示"没有对象",即该处应该有值。

所以 Object.prototype.__ proto__ === null 。 跟 Object.prototype没有原型,表达了一个意思。

最后关系图更新为:

image.png

1.5其他(拓展知识)

1.5.1 constructor

function Father() {}
let son = new Father();
console.log(son.constructor === Father); //true

获取 son.constructor 时,其实son里面没有constructor属性,当不能读取到constructor属性时,就会从son的原型(Father.prototype)中读取,正好原型(__ proto __)中有该属性,所以:

son.constructor === Father.prototype.constructor

1.5.2__proto__

绝大部分浏览器都支持这个非标准的方法访问原型,然而它并不存在于Father.prototype中,实际上,它是来自于Object.prototype,与其说是一个属性,不如说是一个getter/setter,当使用obj.__ proto __时,可以理解成返回了Object.getPrototypeOf(obj)。

1.5.3 继承

关于继承,前面说到“每一个对象都会从原型‘继承’属性”,实际上,继承是一个具有迷惑性的说法, 引用《你不知道的JavaScript》:

继承意味着复制操作,然⽽ JavaScript 默认并不会复制对象的属性,相反,JavaScript 只是在两个对象之间创建⼀个关联,这样,⼀个对象就可以通过委托访问另⼀个对象的属性和函数,所以与其叫继承,委托的说法反⽽更准确些。

2 词法作用域和动态作用域

2.1 作用域

作用域是指程序源代码中定义变量的区域。

作用域规定了如何查找变量,也就是确定当前执行代码对变量的访问权限。

JavaScript采用词法作用域(lexical scoping),也就是静态作用域。

2.2 静态作用域和动态作用域

因为JavaScript采用的是词法作用域,函数的作用域在函数定义的时候就决定了。

而与词法作用域相对的是动态作用域,函数的作用域是在函数调用的时候才决定的。

var a = 1;

function fa() {
  console.log(a);
}

function fb() {
  var a = 2;
  fa();
}

fa();
// 结果是 1

JavaScript采⽤的是静态作⽤域!

大多数现在程序设计语言都是采用静态作用域规则,如C/C++C#PythonJavaJavaScript……

2.3 动态作用域

动态作用域的变量叫做动态变量。只要程序正在执行定义了动态变量的代码段,那么在这段时间内,该变量一直存在;代码段执行结束,该变量便消失。

什么语言是动态作用域的?

采用动态作用域的语言有PascalEmacs LispCommon Lisp(兼有静态作用域)、Perl(兼有静态作用域)。C/C++是静态作用域语言,但在宏中用到的名字,也是动态作用域。

3.执行上下文

3.1顺序执行

JavaScript 的开发者都会有个直观的印象,那就是顺序执⾏:

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

打印的结果却是两个foo2。

这是因为 JavaScript 引擎并⾮⼀⾏⼀⾏地分析和执⾏程序,⽽是⼀段⼀段地分析执⾏。

当执⾏⼀段代码的时候,会进⾏⼀个“准备⼯作”,那这个“⼀段⼀段”中的“段”究竟是怎么划分的呢?

到底JavaScript引擎遇到⼀段怎样的代码时才会做“准备⼯作”呢?

3.2可执行代码

这就要说到 JavaScript 的可执⾏代码( executable code )的类型有哪些了?

其实很简单,就三种,全局代码、函数代码、eval代码。

举个例⼦,当执⾏到⼀个函数的时候,就会进⾏准备⼯作,这⾥的“准备⼯作”,让我们⽤个更专业⼀点

的说法,就叫做"执⾏上下⽂( execution context )"。

3.3执行上下文栈

JavaScript 引擎创建了执⾏上下⽂栈(Execution context stack,ECS)来管理执⾏上下⽂。

为了模拟执⾏上下⽂栈的⾏为,让我们定义执⾏上下⽂栈是⼀个数组:

ECStack = [];

当 JavaScript 开始要解释执⾏代码的时候,最先遇到的就是全局代码,所以初始化的时候⾸先就会向执⾏上下⽂栈压⼊⼀个全局执⾏上下⽂,我们⽤ globalContext 表示它,并且只有当整个应⽤程序结束的时候,ECStack 才会被清空,所以程序结束之前, ECStack 最底部永远有个 globalContext :

ECStack = [
    globalContext
];

例子:

function fun3() {
  console.log("fun3");
}
function fun2() {
  fun3();
}
function fun1() {
  fun2();
}
fun1();

当执⾏⼀个函数的时候,就会创建⼀个执⾏上下⽂,并且压⼊执⾏上下⽂栈,当函数执⾏完毕的时候,就会将函数的执⾏上下⽂从栈中弹出。知道了这样的⼯作原理,让我们来看看如何处理上⾯这段代码:

// 伪代码

// fun1()
ECStack.push(<fun1> functionContext);

// fun1中竟然调⽤了fun2,还要创建fun2的执⾏上下⽂
ECStack.push(<fun2> functionContext);

// 擦,fun2还调⽤了fun3!
ECStack.push(<fun3> functionContext);

// fun3执⾏完毕
ECStack.pop();
// fun2执⾏完毕
ECStack.pop();
// fun1执⾏完毕
ECStack.pop();

// javascript接着执⾏下⾯的代码,但是ECStack底层永远有个globalContext

3.4 回顾 词法作用域

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

两段代码执⾏的结果⼀样,但是两段代码究竟有哪些不同呢?

答案就是执⾏上下⽂栈的变化不⼀样。

模拟 case1:

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

模拟 case2

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