背景:对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的原型
proto
该属性是每个JavaScript对象所具有的属性,会指向该对象的原型
承接上文
有
console.log(person.__proto__ === Person.prototype) // true;
同样也有一个construct函数指向原构造函数
console.log(Person === Person.prototype.construct) // true;
实例和原型的关系
当我们想去读取实例的属性时,如果找不到实例的属性,就去找与实例关联的原型的属性,如果还找不到,就找原型的原型,就这样不断向上递归,找到最顶层为止
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
实例和原型的具体关系如下:
其中蓝色的线就是原型链
词法作用域和动态作用域
作用域决定了当前代码对变量的访问权限
词法作用域即静态作用域,函数的作用域在函数创建时决定。
动态作用域,函数的作用域在函数调用的时候决定。
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"
}
总结
- 全局上下文的变量对象初始化是全局对象
- 函数上下文变量对象的初始化是argument对象
- 在进入函数上下文后添加形参,变量声明,函数声明的属性值
- 在执行代码阶段,会再次修改变量对象属性值
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();
执行过程如下:
- 函数被创建,保存作用域链到内部属性
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的中文版地址是(http://yanhaijing.com/es5/#115)
ECMAScript有语言类型和规范类型两种类型
语言类型就是开发者可以可以直接操作的,比如:undefined,null,string,number等等类型
而规范类型是用算法描述ECMAScript语言结构和语言类型的
接下来主要介绍规范类型中的Reference
Reference
根据ECMAScript里所述,Reference是用来解释delete,typeof以及赋值等操作行为的
尤大是这么说的
这里的 Reference 是一个 Specification Type,也就是 “只存在于规范里的抽象类型”。它们是为了更好地描述语言的底层行为逻辑才存在的,但并不存在于实际的 js 代码中.
Reference 有三个组成部分
- 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的取值
从规范中可以得知如下
- 计算MemberExpression的结果赋值给ref
- 判断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();
现在我们来通过上下文的角度重新分析一下这段代码
- 创建全局上下文,压入上下文栈:ECSstack = [globalContext]
- 全局上下文初始化
globalContext = {
VO: [global],
Scope: [globalContext.VO],
this: globalContext.VO
}
初始化的同时,checkscope函数被创建,并保存作用域链到内部属性
Checkscope.[[scope]] = {
globalContext.VO
}
- 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 ]]
-
创建f函数执行上下文,f函数被压入上下文栈
ECStack = [
fContext,
checkscopeContext,
globalContext
]; -
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')
在之前的介绍中我们其实已经对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);
开发人员普遍认为寄生组合式继承是引用类型比较理想的继承范式