背景:对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. Luogu1879 [USACO06NOV]玉米田Corn Fields (状压DP)

    曾经放弃的坑,都是坑 #include <iostream> #include <cstdio> #include <cstring> #include <a ...

  2. MyBatis 03 缓存

    简介 什么是缓存 存在内存中的临时数据. 将用户经常查询的数据放在缓存(内存)中,用户去查询数据就不用从磁盘上(关系型数据库数据文件)查询,转从缓存中查询,从而提高查询效率,解决了高并发系统的性能问题 ...

  3. discuz怎么转wordpress,详细实操过程

    因为原来的是Discuz! X3.4论坛,目前访问不了,但里面有两个栏目是比较有用的,一个付费栏目,另一个免费栏目,放在硬盘有点可惜,于是想把它转为wordpress的两个栏目.发现网上都没有详细过程 ...

  4. 浅析websocket的基本应用spring boot + vue +C# + WPF

    1.基本概念 首先websocket是基于H5的一种通信.在网页中如果定时获取服务器端的实时数据,我们常采用long poll 和ajax轮询的方式.但是在轮询过程中,由于根本没有新数据的改变,而造成 ...

  5. SpringCache的基本使用

    SpringCache SpringCache是一个框架,实现了基于注解的缓存功能.SpringCache提供了一层抽象,底层可以切换不同的cache实现.具体是通过CacheManager接口来统一 ...

  6. 用Python实现广度优先搜索

    图是一种善于处理关系型数据的数据结构,使用它可以很轻松地表示数据之间是如何关联的 图的实现形式有很多,最简单的方法之一就是用散列表 背景 图有两种经典的遍历方式:广度优先搜索和深度优先搜索.两者是相似 ...

  7. 【android逆向】 ARM for 逆向

    C源码 #include <stdio.h> int nums[5] = {1, 2, 3, 4, 5}; int for1(int n){ //普通for循环 int i = 0; in ...

  8. DFS算法-求集合的所有子集

    目录 1. 题目来源 2. 普通方法 1. 思路 2. 代码 3. 运行结果 3. DFS算法 1. 概念 2. 解题思路 3. 代码 4. 运行结果 4. 对比 1. 题目来源 牛客网,集合的所有子 ...

  9. 引擎之旅 Chapter.1 高分辨率时钟

    目录 游戏中的时间线 真实时间线 游戏时间线 全局时钟的实现方式 我们如何理解时间.在现实生活中,时间就是一个有方向的直线.从一个无穷远到另一个无穷远.用数学去抽象地思考,它就是一个从无穷小到无穷大的 ...

  10. 怎样编写正确、高效的 Dockerfile

    基础镜像 FROM 基础镜像 基础镜像的选择非常关键: 如果关注的是镜像的安全和大小,那么一般会选择 Alpine: 如果关注的是应用的运行稳定性,那么可能会选择 Ubuntu.Debian.Cent ...