JavaScript进阶(Learning Records)

背景:对JavaScript的深入学习
参考:《JavaScript高级程序设计》《冴羽 JavaScript 深入》

从原型到原型链

prototype

prototype是每个函数都会有的属性

function Person(){
}
 Person.prototype.name = 'Kevin';
var person1 = new Person();
var person2 = new Person();
console.log(person1.name) // Kevin
console.log(person2.name) // Kevin

一个函数的prototype指向一个对象,这个对象是构造函数所创建的实例原型
原型是什么:每一个JavaScript对象创建时都会关联另一个对象(除NULL),这个对象就是原型,其他对象从原型继承属性
也是上述例子中person1和person2的原型
img

proto

该属性是每个JavaScript对象所具有的属性,会指向该对象的原型
承接上文

console.log(person.__proto__ === Person.prototype) // true;

img
同样也有一个construct函数指向原构造函数

console.log(Person === Person.prototype.construct) // true;

img

实例和原型的关系

当我们想去读取实例的属性时,如果找不到实例的属性,就去找与实例关联的原型的属性,如果还找不到,就找原型的原型,就这样不断向上递归,找到最顶层为止

function Person() {
 
}
 
Person.prototype.name = 'Kevin';
 
var person = new Person();
 
person.name = 'Daisy';
console.log(person.name) // Daisy
 
delete person.name;
console.log(person.name) // Kevin

实例和原型的具体关系如下:
img
其中蓝色的线就是原型链

词法作用域和动态作用域

作用域决定了当前代码对变量的访问权限
词法作用域即静态作用域,函数的作用域在函数创建时决定。
动态作用域,函数的作用域在函数调用的时候决定。

var value = 1;
 
function foo() {
 console.log(value);
}
 
function bar() {
 var value = 2;
 foo();
}

bar();

由于JavaScript采用的是静态作用域,所以在foo中查找value时会到函数的上层去找,输出是1
如果是动态作用域,就会从调用函数的作用域中找,结果就是2

在《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”(因为其本质都是在执行f())
根据词法作用域,所采用的变量是局部变量

执行上下文栈

可执行代码:有三种,函数代码,全局代码,eval代码
当JavaScript执行到一个函数时,就会进行一定的准备工作(也叫执行上下文)
JavaScript引擎创建了上下文栈(ECS)来方便地管理上下文
让我们来模拟上下文执行地过程
ECS = [] // 初始为空
由于最先遇到的是全局代码globalContext,所以有ECS = [globalContext];
并且会一直存在到程序结束
如果此时遇到下面这段代码

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

工作原理:当执行一个函数时,就会创建一个执行上下文,并且压入执行上下文栈,当函数执行完毕时,就会将执行上下文从栈中弹出
相当于:ECS:[globalContext,fun1,fun2,fun3] ————> ECS:[glovalContext];
只有当调用一个函数时才会创建上下文
再来看两个例子

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

由于JavaScript执行代码是一段一段地执行,并且会优先提取定义的函数式语句并执行
在第二个例子中,第二次声明覆盖了第一次声明,所以都会输出foo2
如果对其中一个进行变量提升,那么结果也会发生改变,这里不再赘述

变量对象

全局上下文

全局上下文中的全局变量指的就是全局对象
在客户端JavaScript中,全局对象就是Windows对象

函数上下文

在函数上下文中,用活动变量表示变量对象(AO)
即在进入函数上下文后,变量对象才会变成活动对象

执行过程

当进入执行上下文时,这时候还没有执行代码
AO是进入函数上下文时被创建的,它通过函数的arguments进行初始化
会包含函数的所有形参,变量声明,函数声明
如遇到下面代码时

function foo(a) {
 var b = 2;
 function c() {}
 var d = function() {};
 
 b = 3;
 
}
 
foo(1);

执行该函数上下文时,这时候的AO是

AO = {
 arguments: {
 0: 1,
 length: 1
 },
 a: 1,
 b: undefined,
 c: reference to function c(){},
 d: undefined
}

代码执行阶段会顺序执行代码,执行完后是

AO = {
 arguments: {
 0: 1,
 length: 1
 },
 a: 1,
 b: 3,
 c: reference to function c(){},
 d: reference to FunctionExpression "d"
}

总结

  1. 全局上下文的变量对象初始化是全局对象
  2. 函数上下文变量对象的初始化是argument对象
  3. 在进入函数上下文后添加形参,变量声明,函数声明的属性值
  4. 在执行代码阶段,会再次修改变量对象属性值
console.log(foo);
 
function foo(){
 console.log("foo");
}
 
var foo = 1;

结果为函数对象,因为在执行上下文时,首先会处理函数声明,然后才处理变量声明,如果之前已经有声明过的变量,则不会发生覆盖

作用域链

作用域链指的是由多个变量对象创建的链表
当查找变量对象时,会优先从当前上下文的变量中查找,如果找不到,会到父级去找(词法作用域)

函数创建

函数内部有一个属性scope,当函数被创建时,会保存所有的父级对象到其中
scope可以表示所有父变量对象的层级链
但是并不代表所有的作用域链

函数激活

函数激活时,进入函数上下文,创建活动变量后,添加到作用域链的顶端
接下来用一个例子来帮助理解

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

执行过程如下:

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

可见,此时作用域链内是全局对象
2.函数执行上下文被压入上下文栈

ECSstack = [
Checkscope,
globalContext
]

3.函数并不立即执行,而是开始做准备工作,
复制函数scope属性创建作用域链
Scope:checkscope.[[scope]]
用arguments创建活动对象,随后初始化活动对象

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

4.将活动对象压入作用域链顶端
Scope:checkscope.[AO,[scope]]
5.准备工作完成,开始执行函数,并且修改AO的值
6.查找到scope2的值,函数返回后结束执行,并从ECS栈中弹出
ECSstack = [globalContext]

从ECMAScript规范解读this

ECMAScript的中文版地址是(//yanhaijing.com/es5/#115)
ECMAScript有语言类型和规范类型两种类型
语言类型就是开发者可以可以直接操作的,比如:undefined,null,string,number等等类型
而规范类型是用算法描述ECMAScript语言结构和语言类型的
接下来主要介绍规范类型中的Reference

Reference

根据ECMAScript里所述,Reference是用来解释delete,typeof以及赋值等操作行为的
尤大是这么说的

这里的 Reference 是一个 Specification Type,也就是 “只存在于规范里的抽象类型”。它们是为了更好地描述语言的底层行为逻辑才存在的,但并不存在于实际的 js 代码中.
Reference 有三个组成部分

  1. base value 2. reference name 3. strict reference
    其中base value 就是属性所在的对象或者EnvironmentRecord,reference name是属性的名称
    下面举两个例子
var foo = 1;
 
// 对应的Reference是:
var fooReference = {
 base: EnvironmentRecord,
 name: 'foo',
 strict: false
};


var foo = {
 bar: function () {
 return this;
 }
};
 
foo.bar(); // foo
 
// bar对应的Reference是:
var BarReference = {
 base: foo,
 propertyName: 'bar',
 strict: false
};

利用getbase可以得到reference的base value,getvalue可以得到该属性具体的值
IsPropertyReference:如果base value是一个对象,返回true

关于this

我们来看看在函数调用的时候,如何确定this的取值
从规范中可以得知如下

  1. 计算MemberExpression的结果赋值给ref
  2. 判断ref是否是一个Reference类型
  • 如果 ref 是 Reference,并且 IsPropertyReference(ref) 是 true, 那么 this 的值为 GetBase(ref)
  • 如果 ref 是 Reference,并且 base value值是 Environment Record, 那么this的值为 ImplicitThisValue(ref)
  • 如果 ref 不是 Reference,那么 this 的值为 undefined
function foo() {
 console.log(this)
}
 
foo(); // MemberExpression 是 foo
 
function foo() {
 return function() {
 console.log(this)
 }
}
 
foo()(); // MemberExpression 是 foo()
 
var foo = {
 bar: function () {
 return this;
 }
}
 
foo.bar(); // MemberExpression 是 foo.bar

原来对MemberExpression的描述就不多赘述,可以简单理解为()左边的部分

var value = 1;
 
var foo = {
 value: 2,
 bar: function () {
 return this.value;
 }
}
 
//示例1
console.log(foo.bar());
//示例2
console.log((foo.bar)());
//示例3
console.log((foo.bar = foo.bar)());
//示例4
console.log((false || foo.bar)());
//示例5
console.log((foo.bar, foo.bar)());
可以看到示例1的MemberExpression是foo.bar,是一个函数
reference是
var Reference = {
 base: foo,
 name: 'bar',
 strict: false
};

可以看到它是第一种情况,this应该指向的是 GetBase(ref),也就是foo,答案为2
对于示例2,加了括号并不会产生影响,所以结果不变
至于示例3,4,5,他们都用了操作符,最后的结果是一个值,所以不是reference,this指向undefined

还有一种情况,就是第二种情况,这时返回的是ImplicitThisValue(ref),该函数总是返回undefined,所以最后this也是指向undefined的(当然个人认为这句话还是有点问题)
例子

function foo() {
  console.log(this);
}
foo(); 

像上面这段代码在本机的输出结果其实是windows全局对象
这是因为当前环境的JavaScript没有使用严格模式
使用严格模式后,值为undefined

执行上下文

那么在了解清楚前面几个东西之后,就可以来看看执行上下文了
对执行上下文来说,有3个重要的属性:
1.变量对象 2.作用域链 3.this
依然给出这个例子

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

现在我们来通过上下文的角度重新分析一下这段代码

  1. 创建全局上下文,压入上下文栈:ECSstack = [globalContext]
  2. 全局上下文初始化
    globalContext = {
    VO: [global],
    Scope: [globalContext.VO],
    this: globalContext.VO
    }

初始化的同时,checkscope函数被创建,并保存作用域链到内部属性
Checkscope.[[scope]] = {
globalContext.VO
}

  1. checkscope执行上下文入栈
    ECStack = [
    checkscopeContext,
    globalContext,
    ];

复制函数[[ scope ]]属性创建作用域链
用argument创建活动对象AO
初始化活动对象,加入形参,函数声明,变量声明
将活动对象压入作用域链顶端

checkscopeContext = {
AO: {
arguments: {
length: 0
},
scope: undefined,
f: reference to function f(){}
},
Scope: [AO, globalContext.VO],
this: undefined
}

在初始化的同时,保存作用域链到f的内部属性 [[ scope ]]

  1. 创建f函数执行上下文,f函数被压入上下文栈
    ECStack = [
    fContext,
    checkscopeContext,
    globalContext
    ];

  2. f函数上下文初始化,跟之前那一步一样

	 fContext = {
	 AO: {
	 arguments: {
	 length: 0
	 }
	 },
	 Scope: [AO, checkscopeContext.AO, globalContext.VO],
	 this: undefined
	 }

后面就是函数执行完赋值弹出出栈的过程

闭包

一般来说,闭包指的是函数+函数所能访问的自由变量
自由变量是除了函数参数和函数中的局部变量,可以在函数中使用的变量
在ECMAScript中,闭包指的是
理论上:所有的函数。因为在创建函数时,其上下文的数据就都被保存起来了,函数在访问全局变量时其实就是在访问自由变量
实践上:即使创建它的上下文已经摧毁,它依然存在(比如内部函数从父函数返回)
在代码中引用了自由变量
引入之前的一个例子

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

在这个例子中,我们可以复习一下之前学习的执行上下文

• 进入全局代码,创建全局执行上下文,全局执行上下文压入执行上下文栈
• 全局执行上下文初始化
• 执行 checkscope 函数,创建 checkscope 函数执行上下文,checkscope 执行上下文被压入执行上下文栈
• checkscope 执行上下文初始化,创建变量对象、作用域链、this等
• checkscope 函数执行完毕,checkscope 执行上下文从执行上下文栈中弹出
• 执行 f 函数,创建 f 函数执行上下文,f 执行上下文被压入执行上下文栈
• f 执行上下文初始化,创建变量对象、作用域链、this等
• f 函数执行完毕,f 函数上下文从执行上下文栈中弹出

可以发现执行f时,checkscope其实已经被销毁了(出栈了)
但是f还是可以通过作用域链找到对应的AO,所以即使checkscopeContext被销毁了,但是JavaScript却能让其AO一直在内存中,这就是实践中的闭包

两个例子:

var data = [];
 
for (var i = 0; i < 3; i++) {
 data[i] = function () {
 console.log(i);
 };
}
 
data[0]();
data[1]();
data[2]();

var data = [];
 
for (var i = 0; i < 3; i++) {
 data[i] = (function (i) {
 return function(){
 console.log(i);
 }
 })(i);
}
 
data[0]();
data[1]();
data[2]();

第一段代码的输出都是3,而第二段代码输出分别为0,1,2
主要的区别就是第二段代码中多了个匿名函数的作用域链,大家可以自行去解读

call,bind浅析

call

var foo = {
 value: 1
};

function bar() {
 console.log(this.value);
}

bar.call(foo); // 1

可以看出,call函数改变了this的指向(指向了foo),并且bar函数也执行了
当foo为null时,视为指向window

bind

bind会创建一个新函数,bind的第一个参数会作为它运行时的this

var foo = {
 value: 1
};
 
function bar() {
 console.log(this.value);
}
 
// 返回了一个函数
var bindFoo = bar.bind(foo); 
 
bindFoo(); // 1

类数组对象和arguments

类数组对象

指拥有一个length属性和若干索引属性的对象

var array = ['name', 'age', 'sex'];
 
var arrayLike = {
 0: 'name',
 1: 'age',
 2: 'sex',
 length: 3
}

可以发现类数组对象和数组的长度,遍历,读写一样
但是类数组对象是不能用数组的方法的
但是类数组可以通过各种方法转化成数组

arguments

function foo(name, age, sex) {
 console.log(arguments);
}
 
foo('name', 'age', 'sex')

img
在之前的介绍中我们其实已经对argument有了一定的了解

length

arguments的length表示实参的个数
它所对应函数的length表示形参的个数

callee

通过该属性函数可以调用自身

var data = [];
 
for (var i = 0; i < 3; i++) {
 (data[i] = function () {
 console.log(arguments.callee.i) 
}).i = i;
}
 
data[0]();
data[1]();
data[2]();
 
// 0
// 1
// 2

非严格模式下,实参和argument的值会共享(绑定)
严格模式下,实参和argument的值不会共享

继承的多种方式以及优缺点

原型链继承

Function Perent()
{
    this.name = 'kevin'
}

Perent.prototype.getName() = function(){
Console.log(this.name)
}
function Child () {
}
 
Child.prototype = new Parent();

var child1 = new Child();
 
console.log(child1.getName()) // kevin

引用类型的属性会被所有实例共享,并且不能向Perent传参

构造函数继承

function Parent () {
 this.names = ['kevin', 'daisy'];
}
 
function Child () {
 Parent.call(this);
}
 
var child1 = new Child();
 
child1.names.push('yayu');
 
console.log(child1.names); // ["kevin", "daisy", "yayu"]
 
var child2 = new Child();
 
console.log(child2.names); // ["kevin", "daisy"]

解决了利用原型链继承的问题
缺点:方法在构造函数中定义,每次创建实例都会创建一遍方法

组合继承

构造函数继承+原型链继承
结合了两者的优点,是常见的继承方式

原型式继承

function createObj(o) {
 function F(){}
 F.prototype = o;
 return new F();
}

同样存在共享的问题,但是在给对象赋值时会优先添加值

寄生式继承

创建一个仅用于封装过程的函数

function createObj (o) {
 var clone = Object.create(o);
 clone.sayName = function () {
 console.log('hi');
 }
 return clone;
}

寄生组合式继承

function Parent (name) {
 this.name = name;
 this.colors = ['red', 'blue', 'green'];
}
 
Parent.prototype.getName = function () {
 console.log(this.name)
}
 
function Child (name, age) {
 Parent.call(this, name);
 this.age = age;
}
 
Child.prototype = new Parent();
 
var child1 = new Child('kevin', '18');
 
console.log(child1)

可以发现其调用了两次父构造函数,一次是new perent,一次是new child
为了避免重复的调用,可以这样做

var F = function () {};
 
F.prototype = Parent.prototype;
 
Child.prototype = new F();
 
 
var child1 = new Child('kevin', '18');
 
console.log(child1);

设置一个空对象作为跳板,即可减少父构造函数的调用
封装过后就是

function object(o) {
 function F() {}
 F.prototype = o;
 return new F();
}
 
function prototype(child, parent) {
 var prototype = object(parent.prototype);
 prototype.constructor = child;
 child.prototype = prototype;
}

当要使用的时候,就prototype(Child, Parent);
开发人员普遍认为寄生组合式继承是引用类型比较理想的继承范式