背景:对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. django中的静态文件

    静态文件 1.什么是静态文件 在django中静态文件是指那些图片.css样式.js样式.视频.音频等静态资源. 2.为什么要配置静态文件 这些静态文件往往不需要频繁的进行变动,如果我们将这些静态文件 ...

  2. 【MySQL】从入门到掌握2-下载安装

    上期:[MySQL]从入门到掌握1-一些背景知识 第一章:下载 官网下载地址: https://dev.mysql.com/downloads/mysql/ https://dev.mysql.com ...

  3. 【python】一些python用法规律笔记

    作为本科用了多年MATLAB的工科生,学起来python有些似曾相识但也有些不习惯的地方. 在这里总结一下,慢慢整理,希望能巩固python语法 一.前闭后开 这个是和MATLAB很大不同.不论是ra ...

  4. HiveSql调优系列之Hive严格模式,如何合理使用Hive严格模式

    目录 综述 1.严格模式 1.1 参数设置 1.2 查看参数 1.3 严格模式限制内容及对应参数设置 2.实际操作 2.1 分区表查询时必须指定分区 2.2 order by必须指定limit 2.3 ...

  5. Go语言学习的坑爹历程

    鄙人暑期实习,需要用Go语言进行编程 在go语言中,结构体的定义只支持变量的声明,成员函数是采用"接口方法"来实现的 留一个成员定义的模板在此 package main impor ...

  6. Fast.Framework ORM 试用

    简介 Fast.Framework 是一款基于 .NET 6 封装的轻量级ORM框架,支持多种数据库(SQL Server.Oracle.MySQL.PostgreSQL.SQLite). 优点 性能 ...

  7. 接入Twitter和Facebook分享踩坑记录

    准备工作 1.首先需要在HTML的head添加下述meta标签内容,在分享时,Twitter和Facebook会爬取该网站页面的meta内容,然后生成分享卡片. 2.按照下述配置完成后,需要把内容发布 ...

  8. 【学习笔记】 Adaboost算法

    前言 之前的学习中也有好几次尝试过学习该算法,但是都无功而返,不仅仅是因为该算法各大博主.大牛的描述都比较晦涩难懂,同时我自己学习过程中也心浮气躁,不能专心. 现如今决定一口气肝到底,这样我明天就可以 ...

  9. nginx中 location正则的理解

    文章转载自:https://blog.csdn.net/wzj_110/article/details/110142902 正则表达式在线测试工具:https://tool.lu/regex loca ...

  10. MySQL 数据更新过程

    文章转载自:https://mp.weixin.qq.com/s?__biz=MzI1MDgwNzQ1MQ==&mid=2247486441&idx=1&sn=fcf93709 ...