背景:对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"
}

总结

  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的中文版地址是(http://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')



在之前的介绍中我们其实已经对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);

开发人员普遍认为寄生组合式继承是引用类型比较理想的继承范式

JavaScript进阶(Learning Records)的更多相关文章

  1. #笔记#JavaScript进阶篇一

    #JavaScript进阶篇 http://www.imooc.com/learn/10 #认识DOM #window对象 浏览器窗口可视区域监测—— 在不同浏览器(PC)都实用的 JavaScrip ...

  2. 4、JavaScript进阶篇①——基础语法

    一.认识JS 你知道吗,Web前端开发师需要掌握什么技术?也许你已经了解HTML标记(也称为结构),知道了CSS样式(也称为表示),会使用HTML+CSS创建一个漂亮的页面,但这还不够,它只是静态页面 ...

  3. JavaScript 进阶(一)JS的"多线程"

    这个系列的文章名为“JavaScript 进阶”,内容涉及JS中容易忽略但是很有用的,偏JS底层的,以及复杂项目中的JS的实践.主要来源于我几年的开发过程中遇到的问题.小弟第一次写博客,写的不好的地方 ...

  4. JavaScript进阶(一)

     OK接下来,我们再次梳理一遍js并且提高一个等级. 众所周知,web前端开发者需要了解html和css,会只用html和css创建一个漂亮的页 面,但是这肯定是不够的,因为它只是一个静态的页面,我们 ...

  5. Javascript 进阶 面向对象编程 继承的一个样例

    Javascript的难点就是面向对象编程,上一篇介绍了Javascript的两种继承方式:Javascript 进阶 继承.这篇使用一个样例来展示js怎样面向对象编程.以及怎样基于类实现继承. 1. ...

  6. JavaScript进阶(九)JS实现本地文件上传至阿里云服务器

    JS实现本地文件上传至阿里云服务器 前言 在前面的博客< JavaScript进阶(八)JS实现图片预览并导入服务器功能>(点击查看详情)中,实现了JS将本地图片文件预览并上传至阿里云服务 ...

  7. JavaScript进阶(十一)JsJava2.0版本

    JavaScript进阶(十一)JsJava2.0版本 2007年9月11日,JsJava团队发布了JsJava2.0版本,该版本不仅增加了许多新的类库,而且参照J2SE1.4,大量使用了类的继承和实 ...

  8. Javascript 进阶 面向对象编程 继承的一个例子

    Javascript的难点就是面向对象编程,上一篇介绍了Javascript的两种继承方式:Javascript 进阶 继承,这篇使用一个例子来展示js如何面向对象编程,以及如何基于类实现继承. 1. ...

  9. javascript进阶笔记(2)

    js是一门函数式语言,因为js的强大威力依赖于是否将其作为函数式语言进行使用.在js中,我们通常要大量使用函数式编程风格.函数式编程专注于:少而精.通常无副作用.将函数作为程序代码的基础构件块. 在函 ...

  10. JavaScript进阶系列07,鼠标事件

    鼠标事件有Keydown, Keyup, Keypress,但Keypress与Keydown和Keyup不同,如果按ctrl, shift, caps lock......等修饰键,不会触发Keyp ...

随机推荐

  1. MySQL数据库授权的两种方式

    方法一:grant命令创建用户并授权(针对只修改权限) grant命令简单语法如下: grant all privileges on dbname.* to username@localhost id ...

  2. postgresql用户与权限管理

    pg使用角色的概念管理数据库访问权限,角色是一系列相关权限的集合.为了管理方便,通常把一系列先关的权限赋予给一个角色,如果哪个用户需要这些权限,就把这些角色赋予给响应的用户. 由于用户也拥有一系列的相 ...

  3. P4675 [BalticOI 2016 day1]Park (并查集)

    题面 在 Byteland 的首都,有一个以围墙包裹的矩形公园,其中以圆形表示游客和树. 公园里有四个入口,分别在四个角落( 1 , 2 , 3 , 4 1, 2, 3, 4 1,2,3,4 分别对应 ...

  4. [CF1519C] Berland Regional (数论分块)

    题面 有 n 个学生和 n 所大学,每个学生在其中一所大学中学习,且各有一个能力值 s i s_i si​ . 某次组队打比赛的召集令会给一个数字 k ,表示团队数量.然后每所大学会先把自己的所有学生 ...

  5. 基于开源方案构建统一的文件在线预览与office协同编辑平台的架构与实现历程

    大家好,又见面了. 在构建业务系统的时候,经常会涉及到对附件的支持,继而又会引申出对附件在线预览.在线编辑.多人协同编辑等种种能力的诉求. 对于人力不是特别充裕.或者项目投入预期规划不是特别大的公司或 ...

  6. 记一次 .NET 某金融企业 WPF 程序卡死分析

    一:背景 1. 讲故事 前段时间遇到了一个难度比较高的 dump,经过几个小时的探索,终于给找出来了,在这里做一下整理,希望对大家有所帮助,对自己也是一个总结,好了,老规矩,上 WinDBG 说话. ...

  7. Databend 源码阅读系列(二):Query server 启动,Session 管理及请求处理

    query 启动入口 Databend-query server 的启动入口在 databend/src/binaries/query/main.rs 下,在初始化配置之后,它会创建一个 Global ...

  8. 宝塔面板服务器ip地址修改域名

    参考博客:请点击百度 今天登录宝塔面板是突然忘记了服务器IP地址,从而导致了以下这种情况. 其实我以前是买过一个域名的,但是并没有绑定到宝塔上面.从而就一直拿IP登录宝塔面版.现在用命令方式更换域名, ...

  9. 四 多例模式【Multition Pattern】 来自CBF4LIFE 的设计模式

    出现在明朝,那三国期间的算不算,不算,各自称帝,各有各的地盘,国号不同.大家还记得那首诗<石灰吟>吗?作者是谁?于谦,他是被谁杀死的?明英宗朱祁镇,对,就是那个在土木堡之变中被瓦刺俘虏的皇 ...

  10. DispatcherServlet 分发流程

    0 太长不看版 HTTPServlet 的 Service 方法将请求按类进行分解 主要是根据HTTP方法的类型调用 doXXX 方法 GET 和 HEAD 方法需要对 if-modified-sin ...