前端架构能力——JS的核心
主要知识:对象、原型链、构造函数、执行上下文堆栈、执行上下文、变量对象、活动对象、作用域链、闭包、this
预热:
什么是爆栈?
// 堆栈溢出 -> 自己调用自己, 是否会存在堆栈溢出错误? =》 会存在
function foo(){
foo();
setTimeout(foo, 0) // 解决堆栈溢出,进行异步处理这个函数, 但是会在另开的子线程中堆栈溢出, 主线程不受影响
}
foo();
JS是一个单线程的?
这个说法是错误的,JS严格的来说并不是一个单线程,JS执行的时候,是有一个主线程的,在执行主线程的时候,可以开启异步,也就是相当于开启了一个子线程去处理别的内容,同时需要注意的一点是,浏览器也可以说是有多个进程的概念
。
对象:
ECMAScript作为一个高度抽象的面向对象语言,是通过对象来交互的,即使ECMAScript里面也有基本类型,但是,当需要的时候,他们也会被转换成对象。
一个对象就是一个属性的集合,并且拥有一个独立的prototype(原型)对象,这个prototype可以是一个对象或者null。
例子:
var foo = {
x:10,
y:20
};
我们有一个对象foo,在其中有两个自身的属性,以及一个隐含的__proto__
属性,而这个__proto__属性,我们称之为:原型链指针
,它连接到foo的原型对象上,也就是对原型对象的引用,也就代表着,我们可以使用原型对象上面的方法。
原型链
其实原型对象也只是一个简单的对象,并且它们也是可以拥有自己的原型的,如果一个原型对象的原型是一个非null(也就是代表引用了另外一个原型对象
)的引用,那么以此类推,这就叫作原型链。
原型链是一个用来实现继承和共享属性的有限对象链
考虑这么一个情况,我们拥有两个对象,它们之间只有一小部分不同,其他部分都相同。显然,对于一个设计良好的系统,我们将会重用相似的功能/代码,而不是在每个单独的对象中重复它。在基于类的系统中,这个代码重用风格叫作类继承-你把相似的功能放入类A中,然后类 B和类 C继承类 A,并且拥有它们自己的一些小的额外变动。
ECMAScript中没有类的概念。但是,代码重用的风格并没有太多不同(尽管从某些方面来说比基于类(class-based)的方式要更加灵活)并且通过原型链来实现。这种继承方式叫作委托继承(delegation based inheritance)(或者,更贴近ECMAScript一些,叫作原型继承(prototype based inheritance))。
首先来看一个简单的原型链例子
var a = {
x: 10,
calculate: function (z) {
return this.x + this.y + z
}
};
var b = {
y: 20,
__proto__: a
};
var c = {
y: 30,
__proto__: a
};
// call the inherited method
b.calculate(30); // 60
c.calculate(40); // 80
从中,我们看到b和c访问到了在对象a中定义的calculate方法,这是通过原型链实现的。
如果没有明确为一个对象指定原型,那么它将会使用__proto__的默认值
-Object.prototype。Object.prototype对象自身也有一个__proto__属性,这是原型链的终点并且值为null。
ES5标准化了一个实现原型继承的可选方法,即使用Object.create函数:
var b = Object.create(a, {y: {value: 20}});
var c = Object.create(a, {y: {value: 30}});
而在ES6中对__proto__
更加的标准化,并且可以在对象初始化的时候使用它,这也就引出了我们下面要说的构造函数
构造函数
除了以指定模式创建对象外,构造函数也做了另一个有用的事情 - 它自动地为新创建的对象设置一个原型对象。这个原型对象存储在ConstructorFunction.prototype属性中
简单来说,我们可以使用构造函数来重写上一个拥有对象b和对象c的例子。因此,对象a(一个原型对象)的角色有Foo.prototype来扮演
// a constructor function
function Foo(y) {
// which may create objects
// by specified pattern: they have after
// creation own "y" property
this.y = y;
}
// also "Foo.prototype" stores reference
// to the prototype of newly created objects,
// so we may use it to define shared/inherited
// properties or methods, so the same as in
// previous example we have:
// inherited property "x"
Foo.prototype.x = 10;
// and inherited method "calculate"
Foo.prototype.calculate = function (z) {
return this.x + this.y + z;
};
// now create our "b" and "c"
// objects using "pattern" Foo
var b = new Foo(20);
var c = new Foo(30);
// call the inherited method
b.calculate(30); // 60
c.calculate(40); // 80
// let's show that we reference
// properties we expect
console.log(
b.__proto__ === Foo.prototype, // true
c.__proto__ === Foo.prototype, // true
// also "Foo.prototype" automatically creates
// a special property "constructor", which is a
// reference to the constructor function itself;
// instances "b" and "c" may found it via
// delegation and use to check their constructor
b.constructor === Foo, // true
c.constructor === Foo, // true
Foo.prototype.constructor === Foo, // true
b.calculate === b.__proto__.calculate, // true
b.__proto__.calculate === Foo.prototype.calculate // true
);
代码关系如下:
这张图又一次说明了每个对象都有一个原型。构造函数Foo也有自己的__proto__,值为Function.prototype,Function.prototype也通过其__proto__属性关联到Object.prototype。因此,重申一下,Foo.prototype就是Foo的一个明确的属性,指向对象b和对象c的原型。
注意:在ES6中 类的概念被标准化了,并且实际上以一种构建在构造函数上面的语法糖来实现,就像上面描述的一样。从这个角度来看原型链成为类继承的一种具体实现方式
下面的这段代码为ES6的类概念形成的继承以及属性共享的功能
// ES6
class Foo {
constructor(name) {
this._name = name;
}
getName() {
return this._name;
}
}
class Bar extends Foo {
getName() {
return super.getName() + ' Doe';
}
}
var bar = new Bar('John');
console.log(bar.getName()); // John Doe在这里插入代码片
现在,我们知道了对象、原型链的基础之后、让我们来看看运行时程序的执行,在ECMAScript中时如何实现的,这种执行叫做执行上下文栈,其中每个元素可以轴向成为一个对象。
执行上下文堆栈
这里有三种类型的ECMAScript代码:全局代码、函数代码和eval代码。每个代码是在其执行上下文中求值的。这里只有一个全局上下文,可能有多个函数执行上下文以及eval执行上下文,对一个函数的每次调用,会进入到函数执行上下文中,并对函数代码类型进行求值,每次对eval函数进行调用,会进入eval执行上下文并对其代码进行求值。
注意:一个函数可能会创建无数的上下文,因为对函数的每次调用(即使这个函数递归的调用自己)都会生成一个具有新状态的上下文
function foo(bar) {}
// call the same function,
// generate three different
// contexts in each call, with
// different context state (e.g. value
// of the "bar" argument)
foo(10);
foo(20);
foo(30);
一个执行上下文可能会触发另外一个上下文,比如,一个函数调用另一个函数(或者在全局上下文中调用一个全局函数)等等。从逻辑上来说,这是以栈的形式实现的,它叫做执行上下文栈。
一个触发其他上下文的上下文叫做caller。被触发的上下文叫做collee。collee在同一时间可能是其他collee的coller(比如,一个全局上下文中被调用的函数,之后调用了一些内部函数)
当一个caller触发(调用)了一个callee,这个caller会暂缓自身的执行,然后把控制权传递给callee。这个callee被push到栈中,并成为一个运行中的执行上下文。在callee的上下文结束后,它会把控制权返回给caller,然后caller的上下文继续执行直到它结束,以此类推,callee可能简单的返回或者由于异常而退出。一个抛出的但是没有被捕获的异常可能退出一个或者多个上下文。
执行上下文堆栈 = 多个函数的调用,(在调用的同时,全局上下文函数可能会调用局部上下文函数)
像我们说的,栈中的每个执行上下文都可以用一个对象来表示
执行上下文
首先问个问题,JS代码是如何执行的?
简易的一个图:
在这里我们主要关注的一点就是执行上下文那里,执行上下文在整个JS代码的执行中占据着重要的地位
一个执行上下文可以抽象的表示为一个简单的对象。每一个执行上下文拥有一些属性用来跟踪和它相关的代码的执行过程
对于构成执行上下文,有三个重要的属性
变量对象(Variable object, VO)
作用域链(Scope chain)
this
这三大部分构成了执行上下文,而执行上下文的存在,是取决于调用,只有被调用了后,才会出现执行上下文的概念
变量对象
变量对象是与执行上下文相关的数据作用域,它是一个与上下文相关的特殊对象,其中存储了在上下文中定义的变量和函数声明,是一个抽象的概念,对于不同的上下文类型,使用不同的对象。
注意:函数表达式不包含在变量对象中
var foo = 10;
function bar() {} // function declaration, FD
(function baz() {}); // function expression, FE
console.log(
this.foo == foo, // true
window.bar == bar // true
);
console.log(baz); // ReferenceError, "baz" is not defined
在这个例子中,我们可以看到全局上下文变量对象
分别是:foo以及bar(),而baz则是一个函数表达式,不是变量对象
与其它语言(比如C/C++)相比,在ECMAScript中只有函数可以创建一个新的作用域,在函数作用域中所定义的变量和内部函数在函数外面是无法直接访问的,而且不会污染全局变量对象
那么函数和它的变量对象是怎么样的?在函数上下文中,变量对象是以活动对象来表示的。
活动对象
当一个函数被caller所触发(调用),一个特殊的对象昂,叫做活动对象(activation object)将会被创建。这个对象中包含形参和那个特殊的arguments对象(是对形参的一个映射,但是值是通过索引来获取)。活动对象之后会作为函数上下文的变量对象来使用
简单的说:函数的变量对象也是一个同样简单的变量对象,但是除了变量和函数声明外,它还存储了形参和arguments对象,并叫做活动对象
function foo(x, y) {
var z = 30;
function bar() {} // FD
(function baz() {}); // FE
}
foo(10, 20);
在这里例子当中,它的活动对象(函数上下文变量对象)分别为:x,y,arguments,z,bar() 而baz作为一个函数表达式,不在活动对象的范围内。
众所周知,在ECMAScript中我们可以使用内部函数,然后在这些内部函数我们可以引用父函数的变量或者全局上下文变量,当我们把变量对象命名为上下文的作用域对象,与上面讨论的原型链相似,这里有一个叫做作用域链的东西。
就上下文而言,标识符指的是:变量名称,函数声明,形参
,等等。当一个函数在其代码中引用一个不是局部变量(或者局部函数或者一个形参)的标识符,那么这个标识符就叫作自由变量。搜索这些自由变量(free variables)正好就要用到作用域链。
通常情况:作用域链是一个包含所有父(函数)变量对象_ _加上(作用域链头部的)函数自身变量/活动对象的一个列表
作用域链
作用域链是一个对象列表,上下文代码中出现的标识符在这个列表中进行查找。
这个规则还是与原型链同样简单以及相似:如果一个变量在函数自身的作用域(在自身的变量/活动对象)中没有找到,那么将会查找它父函数(外层函数)的变量对象,以此类推。
在通常情况下,作用域链是一个包含所有父(函数)变量对象__加上(在作用域链头部的)函数自身变量/活动对象的一个列表。但是,这个作用域链也可以包含任何其他对象,比如,在上下文执行过程中动态加入到作用域链中的对象-像with对象或者特殊的catch从句(catch-clauses)对象。
当解析(查找)一个标识符的时候,会从作用域链中的活动对象开始查找,然后(如果这个标识符在函数自身的活动对象中没有被查找到)向作用域链的上一层查找-重复这个过程,就和原型链一样。
var x = 10;
(function foo() {
var y = 20;
(function bar() {
var z = 30;
// "x" and "y" are "free variables"
// and are found in the next (after
// bar's activation object) object
// of the bar's scope chain
console.log(x + y + z);
})();
})();
我们可以假设通过隐式的__parent__属性来和作用域链对象进行关联,这个属性指向作用域链中的下一个对象。这个
方案可能在真实的Rhino代码中经过了测试,并且这个技术很明确得被用于ES5的词法环境中(在那里被叫作outer连
接)。作用域链的另一个表现方式可以是一个简单的数组。利用__parent__概念,我们可以用下面的图来表现上面的
例子(并且父变量对象存储在函数的[[Scope]]属性中):
在代码执行过程中,作用域链可以通过使用with语句和catch从句对象来增强。并且由于这些对象是简单的对象,它们可以拥有原型(和原型链)。这个事实导致作用域链查找变为两个维度:(1)首先是作用域链连接,然后(2)在每个作用域链连接上-深入作用域链连接的原型链(如果此连接拥有原型)。
Object.prototype.x = 10;
var w = 20;
var y = 30;
(function foo(){
var w = 40;
var x = 100;
with({z:50}){
console.log(w,x,y,z) // 40, 10, 30, 50
}
console.log(w,x) //40, 10
})()
我们可以给出如下的结构(确切的说,在我们查找__parent__连接之前,首先查找__proto__链)
在处理上下文时,要经历两个阶段
进入执行上下文
执行代码
进入执行上下文
进入执行上下文,首先就是VO被一些属性填充(被全局变量以及实现定义的函数填充):
- 函数的形参(当进入函数上下文时)——变量对象的一个属性,其属性名就是传来的值,其值就是实参的值,如果没有就是undefined
- 函数的声明—— 变量对象的一个属性,其属性名和值是函数创建出来的,如果变量对象已经包含了相同名字的属性和值,则替换
- 变量的声明——变量对象的一个属性,其属性名为变量名,其值为undefined,如果变量对象已经包含了相同名字的属性和值,则不会影响已存在的
总结
一个普通对象的prototype是以[[prototype]] 来引用的, 但是,在代码的示意图里,我们会用__proto__ 的来代替这个[[prototype]], 也就是说,对于prototype来说, 他就是__proto__。 prototype是 表达这个对象的原型对象,proto 是具体的实现链接到这个原型对象上面