标签:erro 表达式 多个 进入 a+b 网络 epc 引用类型 面向
该文章是为大家整理一个关于js的知识网络,重点是知识的罗列及之间的联系,所以实例可能会有所不足,导致可能没有对应知识的人看不懂,希望大家能够结合其他资料来学习这篇文章,并整理出自己的知识体系。
ok,我们开始。
JavaScript是解释型语言,这就是说它无需编译,直接由JavaScript引擎直接执行。
既然说到了解释型语言,那么我们就来分别以下解释型语言和编译型语言的差别:
其中程序无需编译,不是说真的不需要编译了,直接执行脚本字符串。而是说不需要在运行之前先编译程序成为exe文件,而是在运行的过程中边运行边执行。
ok,我们回到JavaScript的解析执行过程。
在整体上,JavaScript的解析执行过程分为两个步骤:
其中,编译是在解释器中进行,将代码编译成可执行码。运行是在JavaScript引擎中进行,执行可执行码。
过程如下:
编译过程不必多说,我们只要清楚这个过程会将字符串代码编译为可执行码。
重点是运行过程,运行又由两个过程组成
预解析的工作是
重点注意收集变量这一功能,又名为变量提升,收集的变量有以下三种:
若是变量名有重复的话,按照优先级来确定:
function声明定义>函数参数>var声明的变量
tips:
JS执行是需要分号的,但为什么以下语句却可以正常运行呢?
console.log(‘a‘)
console.log(‘b‘)
正是因为预解析阶段会进行分号补全操作。
列举几条自动加分号的规则:
不过若是以下的情况,必须得加上‘;‘,否则的话,会出现报错。
还有,其实所有代码都可以写在一行中。只要有‘;‘来分隔开每一句就ok。并且,if及for及while的函数体也可以写在同一行中。
只要做好分隔工作,那么就都可以写在同一行。
JavaScript有6种数据类型(暂且不论symbol):Number,Boolean,String,Null,Undefined,Object
其中,分为两大类别
tips:
关于不同类型的数据是如何存储在内存的,参考下图:
需要特别注意的是,如下:
var a = {name:‘Bob‘}
变量a存储的值不是该对象,而是该对象在堆内存中的地址。
再看下面两道题:
// demo01.js
var a = 20;
var b = a;
b = 30;
// 这时a的值是多少?
// demo02.js
var m = { a: 10, b: 20 }
var n = m;
n.a = 15;
// 这时m.a的值是多少
在变量对象中的数据发生复制行为时,系统会自动为新的变量分配一个新值。var b = a执行之后,a与b虽然值都等于20,但是他们其实已经是相互独立互不影响的值了。具体如图。所以我们修改了b的值以后,a的值并不会发生变化。
在demo02中,我们通过var n = m执行一次复制引用类型的操作。引用类型的复制同样也会为新的变量自动分配一个新的值保存在变量对象中,但不同的是,这个新的值,仅仅只是引用类型的一个地址指针。当地址指针相同时,尽管他们相互独立,但是在变量对象中访问到的具体对象实际上是同一个。如图所示。
因此当我改变n时,m也发生了变化。这就是引用类型的特性。
内存是有限的,所以分配的内存必须得在适当的时机回收以供后继使用。
内存的生命周期为:
这第三步对应的就是垃圾回收。
那么JavaScript引擎是如何判断该内存需不需要释放呢——标记清楚机制。
垃圾回收器每隔一段时间都会检查一次内存,找到其中失去引用的变量,并释放掉。
其中失去引用一般有两种原因
tips:
浅拷贝开辟一个新的内存空间,仅拷贝第一层对象内容,深拷贝也开辟一个新的内存空间,拷贝所有层对象堆内容。
var arr1 = [2,4]
let arr2 = arr1
console.log(arr1==arr2)//true
这就是最常见的,但还不是浅拷贝,那如果我们这样呢?
var arr1 = [2,4]
var arr2 = []
for(let i in arr1){//该复制方式既适用于复制数组又适用于复制对象
arr2[i]=arr1[i]
}
console.log(arr1==arr2)//false
这样输出的是false,也就是说arr1和arr2指向的地址不同。那么这样就是深拷贝了吗?
不是的。如果arr1的成员中有个对象呢?那么对该对象的复制就是浅拷贝。
那么究竟如何才能做到浅拷贝呢?使用递归,每一次递归进行一次如上的拷贝,直到当前层递归数据为非对象。
var arr1 = [2,4,{name:‘bob‘},[323,4342]]
function deepCopy(val){
var arrSec
val instanceof Array ? arrSec =[] : arrSec ={}
for(let i in val){
else if(val[i] instanceof Object){
arrSec[i] = deepCopy(val[i])
}
else{
arrSec[i] = val[i]
}
}
return arrSec
}
var arr2 = deepCopy(arr1)
无需底层实现的浅拷贝与深拷贝:
浅拷贝:(以下方法仅适用于数组的浅拷贝)
深拷贝:(这个方法既适用于对象又适用于数组)
使用typeof来检测基本类型,用instanceof来检测对象还是数组
数据类型有:
number,string,boolean,null,undefined,object,function,array
typeof一般只能返回如下结果:
number,string,boolean,object(null,object,array),function,undefined
tips:
由于引用类型的数据用typeof返回的都是object(除function),所以我们用instanceof来判断究竟是什么引用类型(这个说法不是很严谨,大家可以不要记忆这个概念)。
instanceof的使用一般是左值为对象,右值为构造函数。
判断方法如下:
沿着左值对象的__proto__这条线走,并且沿着右值构造函数的prototype这条线走,只要两者能够交叉,即同一个对象,那么就返回true。如果__proto__这条线已经走到头了,还未交叉,则返回false。
所以说,与其说instanceof判断的是什么引用类型,倒不如说是判断是否有继承关系。
大家应该都有接触过函数调用栈吧,执行上下文就是每次压入栈的内容。
执行上下文可以理解为当前代码的执行环境,它会形成一个作用域。
JavaScript的执行环境大致可以分为三种:
所以,JavaScript只有全局作用域及函数作用域。
所以,我们可以这样理解——当开始执行JavaScript代码时,会创建一个全局上下文。每当执行一个函数,就会创建一个函数执行上下文。
JavaScript引擎会以栈的方式处理它们,这个栈我们称为函数调用栈。栈底永远是全局上下文,栈顶就是当前正在执行的执行上下文。
所以,统一一下——当开始执行JavaScript代码时,创建一个全局上下文,压入函数调用栈。每当执行一个函数,就会创建一个函数执行上下文,压入函数调用栈。当函数执行完,该执行上下文弹出栈。直到关闭该页面,才会弹出全局上下文。
tips:
执行上下文的生命周期可以分为两个阶段:
执行上下文由三部分组成:
该对象存储的就是变量提升的arguments参数,var声明的变量,函数声明。
在未进入执行阶段时,变量对象(VO variable Object)中的属性都不能访问。但在进入执行阶段时,变量对象转换为了活动对象(AO active Object),里面的属性都能被访问。
VO和AO其实都是一个对象,只是处于执行上下文的不同生命周期。只有在函数调用栈的顶部执行上下文的变量对象才会变成变量对象。
由该环境和所有父环境的变量对象组成的链式结构,保证了当前执行环境对符合访问权限的变量和函数的有序访问
我们通过作用域链,遍历自身的变量对象到全局对象,直到找到对应的变量。
理解作用域链非常关键,这是理解闭包的基础。
执行上下文和作用域是两个完全不同的概念。作用域是在编译阶段就确定下来的,执行上下文是在执行阶段才能够创建的。
不过,切记,当前作用域和上层作用域不是包含关系。
关于垃圾回收机制,有一个重要的行为,那就是,当一个值,在内存中失去引用时,垃圾回收机制会根据特殊的算法找到它,并将其回收,释放内存。
而我们知道,函数的执行上下文,在执行完毕之后,生命周期结束,那么该函数的执行上下文就会失去引用。其占用的内存空间很快就会被垃圾回收器释放。可是闭包的存在,会阻止这一过程。
实现闭包的操作:
闭包的核心就是——通过在外部函数(B)的外部(C)保存内部函数(A)的引用,当执行该引用(A)时,由于创建的执行上下文的作用域链中包含有外部函数(B)的引用,从而使外部函数(B)的执行上下文不会被垃圾回收。
这样就能保存之前执行函数B的操作结果。这样的话,就可以在其他的执行上下文中,操作到函数B的操作结果。
要切记哦:虽然函数A被保存在了函数C中,但函数A的作用域链并没有变化,千万不要把作用域链和函数调用栈混在一起了。在闭包中,能访问到的仍然是作用域链上能查询到的数据。
闭包返回的作用域链中,中间层及之前层的都是不变的内存区域,只有最高层的变量对象是每次调用函数的时候新创建的变量对象。
关于this的指向一直是大家比较头疼的地方,似乎很难找到一个确切的标准。但this的指向还是有标准的,且往下看。
this的执行是在调用函数,即执行上下文创建时才能确定的,判断标准如下:
new到底做了什么呢?
这两者的作用都是修改函数中的this指向,功能一直,只是参数的写法有稍许不同。
call传参需要一个一个地传,而apply传参是传一个数组。
而bind和call,apply的区别在于:
call,apply会直接执行。bind是在函数调用之前,改变this的指向,它会返回一个函数。
箭头函数是ES6的新语法,形式如下:
(参数部分)=>{
函数体部分
}
其中,如果参数只有一个,则可以省略括号。如果没有参数或多个参数,括号不能省略。
箭头函数中this指向规则与普通函数的规则不同,他的this指向规则为:
捕获其所在(即定义的位置)上下文的this值, 作为自己的this值,
tips:
function Person() {
console.log(this)
setTimeout(() => {
// 回调里面的 `this` 变量就指向了期望的那个对象了
console.log(this)
}, 3000);
}
var p = new Person();
普通函数的this指向和setTimeout中的箭头函数的指向都是Person对象。
JavaScript是一门面向过程的语言,但随着网页需求功能的复杂化,工程化,要求JavaScript应该也有面向对象编程的能力。
我们可以通过字面量对象来创建一个简单对象
var obj = {}
当我们想要给我们创建的简单对象添加方法时,可以这样表示
// 可以这样
var person = {};
person.name = "TOM";
person.getName = function() {
return this.name;
}
// 也可以这样
var person = {
name: "TOM",
getName: function() {
return this.name;
}
}
访问属性的时候,可以用一下两种方式
person.name
// 或者
person[‘name‘]
当我们想要用一个变量值来作为属性名来访问属性,就用第二种方法。
使用上面的方式创建对象很简单,但是在很多时候并不能满足我们的需求。就以person对象为例。假如我们在实际开发中,不仅仅需要一个名字叫做TOM的person对象,同时还需要另外一个名为Jake的person对象,虽然他们有很多相似之处,但是我们不得不重复写两次。
var perTom = {
name: ‘TOM‘,
age: 20,
getName: function() {
return this.name
}
};
var perJake = {
name: ‘Jake‘,
age: 22,
getName: function() {
return this.name
}
}
显然,这样是很不合理的,当有太多的相似对象,编写代码会极为痛苦。
这就引出了工厂模式。
工厂模式就是你给出原料,然后返回给你产品。
看代码:
var createPerson = function (name,age){
//创建一个中间对象
var obj = new Object()
obj.name = name
obj.age = age
obj.getName = function(){
return this.name
}
}
var Tom = createPerson(‘Tom‘,18)
var Cherry = createPerson(‘Tom‘,40)
不要把工厂模式想的太高大上。显然,工厂模式帮我们解决了重复编码的麻烦,但是他还有一个问题
无法识别工厂模式返回的对象的类型。(其次还有每次返回对象都得为方法分配一个新的内存空间,浪费资源)
如上述代码,Tom和Cherry指向的对象类型都是Object类型。
首先构造函数就是个普通的函数,其本身没有什么特别的地方。
但构造函数的特殊之处就在于用new创建一个对象,构造函数对该对象的属性进行添加。
new的具体过程在上文有详细提到,就不赘述了。
就这样,new关键字+构造函数就能够创建出一个有属性的对象,且还能够识别对象类型。
但是又有一个问题来了:
所有用该构造函数创建的对象访问的方法实现是一模一样的,但是每次new的时候都会在内存中分配一片新的空间以保存变量的特性和方法。
显然这是不合理的,既然访问的是同一个方法实现,那么为什么不能每个实例对象都访问同一块内存里的方法呢?
我们创建的每一个函数,都有prototype属性指向原型对象,可以选择在原型对象里挂载属性和方法,这样每创建一个对象,都可以通过__proto__访问到原型对象,也就不需要再为这些属性和变量分配空间了。
由于每个函数都可以是构造函数,每个对象都可以是原型对象,因此如果在理解原型之初就想的太多太复杂的话,反而会阻碍你的理解,这里我们要学会先简化它们。就单纯的剖析这三者的关系。
// 声明构造函数
function Person(name, age) {
this.name = name;
this.age = age;
}
// 通过prototye属性,将方法挂载到原型对象上
Person.prototype.getName = function() {
return this.name;
}
var p1 = new Person(‘tim‘, 10);
var p2 = new Person(‘jak‘, 22);
console.log(p1.getName === p2.getName); // true
如图
通过图示我们可以看出,构造函数的prototype与所有实例对象的__proto__都指向原型对象。而原型对象的constructor指向构造函数。
可以这样理解:
构造函数中this添加的属性和方法是私有属性和方法(虽然这个私有属性和方法能够被外界直接取到),原型对象中的属性和方法是共有属性和方法。
当我们访问实例对象中的属性或者方法时,会优先访问实例对象自身的属性和方法,即私有属性和方法。如若找不到,则去原型对象中寻找。
原型链如图:
每一个对象既可以作为原型对象,又可以作为实例对象,而且有可能既是实例对象又是全局对象,这样的一个对象正是原型链中的一个节点。
继承分为两个步骤:
具体代码如下:
var Person = function(name){
this.name = name
}
Person.prototype.getName = function(){
return this.name
}
var cPerson = function(name,age){
Person.call(this,name)
this.age = age
}
cPerson.prototype = new Person(‘名称‘)
//在子级的原型里添加更多的方法
cPerson.prototype.moreFunc = function(){
console.log(‘更多的方法‘)
}
var p = new cPerson(‘Tom‘,18)
该继承方案有一个问题,就是
子级的原型对象是父构造函数的实例对象,这样的话,我们就调用了两次父级的构造函数(子级对象中的将子级原型对象中的给屏蔽了)。
子级原型对象为父级实例对象,实际目的仅是父级实例对象中的__proto__,从而形成原型链来寻找属性和方法。
重复调用两次父级的构造函数是没有意义的,所以我们改进一下代码。
var Person = function(name){
this.name = name
}
Person.prototype.getName = function(){
return this.name
}
var cPerson = function(name,age){
Person.call(this,name)
this.age = age
}
(function (){
var Super = function (){}
Super.prototype = Person.prototype
cPerson.prototype = new Super()
//在子级的原型里添加更多的方法
cPerson.prototype.moreFunc = function(){
console.log(‘更多的方法‘)
}
})()
var p = new cPerson(‘Tom‘,18)
这就是继承的最终解决方案了。
前面说过,js只有全局作用域及函数作用域,没有块级作用域。
但是let和const会引入块级作用域。
这两者的特点为:
我们常常使用let来声明一个值会被改变的变量,而使用const来声明一个值不会被改变的变量,也可以称之为常量。
以一个例子比对一下大家就知道了
// es6
const a = 20;
const b = 30;
const string = `${a}+${b}=${a+b}`;
// es5
var a = 20;
var b = 30;
var string = a + "+" + b + "=" + (a + b);
使用``将整个字符串包起来,在其中使用${}包裹一个变量或表达式
同样,以一个例子来解释
// 首先有这么一个对象
const props = {
className: ‘tiger-button‘,
loading: false,
clicked: true,
disabled: ‘disabled‘
}
// es5
var loading = props.loading;
var clicked = props.clicked;
// es6
const { loading, clicked } = props;
// 给一个默认值,当props对象中找不到loading时,loading就等于该默认值
const { loading = false, clicked } = props;
是不是很简单,就是将访问属性与变量命名在写法上合并为一步。
另外,数组也有属于自己的解析结构
// es6
const arr = [1, 2, 3];
const [a, b, c] = arr;
// es5
var arr = [1, 2, 3];
var a = arr[0];
var b = arr[1];
var c = arr[2];
数组以序列号一一对应,这是一个有序的对应关系。
而对象根据属性名一一对应,这是一个无序的对应关系。
在ES6中用...来表示展开运算符,它可以将数组或者对象进行展开。
const arr1 = [1, 2, 3];
const arr2 = [...arr1, 10, 20, 30];
// 这样,arr2 就变成了[1, 2, 3, 10, 20, 30];
const obj1 = {
a: 1,
b: 2,
c: 3
}
const obj2 = {
...obj1,
d: 4,
e: 5,
f: 6
}
// 结果类似于 const obj2 = Object.assign({}, obj1, {d: 4})
展开字符串还可以运用在参数中
ES6对对象字面量做了简化语法的处理。
var name = ‘Bob‘
var age = 20
var arc = ‘sex‘
var person = {
name,
age,
[arc]:‘male‘
getName(){
return this.name
}
}
class是ES6的新功能。JavaScript创建类的方式是构造函数,这对于普通的面向对象程序员来说太过别树一帜了。
所以,ES6模拟普通面向对象的类-对象编写代码方式,创造了class。
class实际上就是一个var声明变量,其指向其中的constructor构造函数,只是在原先的构造函数创建类的模式做了形式上的变形,使更符合JAVA的类-对象模式。
知识点:
其中,constructor函数就是原先的构造函数,当new一个对象的时候就是默认调用这个函数,因为类名就是指向的constructor函数。
构造函数.prototype.方法名=...
来声明。
类名.prototype.方法名=...
来在类外动态为类定义方法。return new A()
class类不会变量提升
注意:
使用extends关键字。
然后在子的constructor函数中使用super()来调用父的构造函数,从而复制父的实例属性。继承过后,通过原型链,可以访问到父级的所有原型链(即可访问到以往所有被用prototype声明的对象)内容。
super在子类定义中有两种使用方法:
注意:
静态方法的意义是:在该类所有实例之上,只属于类本身的方法,不是具体某个实例的方法,也不是所有实例都有的方法。
切记:静态方法的意义不是省空间(这是原型方法的意义,而且实例根据原型链访问不到静态方法),而是一个能够统领全局(中枢权)的方法。
由于静态方法中的this指向类本身,所以我们无法使用this来访问到实例属性,方法。但是,我们可以通过创建一个实例对象,从而访问到实例属性,方法。
class Foo{
prop:2//错
static prop:2//错
prop=2//错
var prop = 2//错
}
关于私有,静态,原型,实例属性和方法,在类中get及set声明函数代码如下:
var prot = ‘prot‘
class MyClass {
constructor() {
this[bar]()
//类内 构造函数内 public实例属性,方法
this.entity = 0
this.entityFunc = ()=>{
MyClass.staticFunc2()
}
this.entityFunc2 = ()=>{
console.log(‘aa‘)
}
//类内 构造函数内 私有属性,方法
var privat = 8
var privateFunc = function(){return 6}
this.outFunc = function(){
//操作私有变量,使用私有函数
privat = 88
privateFunc()
}
console.log(this.name); // 42
}
// 类内 get,set拦截读写
get prop(){
return 9
}
set prop(par){
this._prop = 8
}
//类中 构造函数外 原型方法
[prot](){}
}
//类外 静态属性,方法
MyClass.static = 9
MyClass.staticFunc = function(){
console.log(‘static‘)
MyClass.staticFunc2()
}
MyClass.staticFunc2 = function(){
console.log(‘static2‘)
}
//类外 原型属性,方法
MyClass.prototype.prott = ()=>3
MyClass.prototype.prottt = 5
console.log(Object.getOwnPropertyDescriptor(MyClass,‘staticFunc2‘))
console.log(MyClass.name)
tips:
[变量]:值
,当要调用这个属性时,对象[变量]
class中声明方法:
class Box{
constructor(msg){
this.msg = msg
}
save(){
console.log(this.msg)
}
}
对象中声明方法:
var Box = {
save:function(){
console.log(‘aaa‘)
}
}
我们要保证异步的顺序执行,在以往都是通过回调函数来实现。
然而回调函数有两个缺陷:
这两者都会导致阅读源码的困难。
所以,我们需要新的方案能够有以下两个特点:
Promise能做到这两点。
Promise对象有三种状态:
这三种状态不受外界影响,而且状态只能是从pending到resolved或者pending到rejected。我们通过Promise构造函数中的第一个函数参数来处理状态改变。
new Promise(function(resolve, reject) {
if(true) { resolve() };
if(false) { reject() };
})
tips:
Promise对象中的then方法,可以接受到Promise对象的状态变化。then方法有两个参数,第一个参数接受resolved状态的执行,第二个参数接受rejected状态与异常状态的执行。
又因为then方法执行完后会返回一个新建的Promise对象,所以可以继续用then来接受Promise对象的状态变化,并执行相应函数。这也是扁平式代码结构的核心。
catch方法是.then(null, rejection)的别名,用于接受rejected状态与异常状态的执行。
Promise对象的rejected状态与异常状态具有冒泡性质,一直向后传递,直到被捕获为止。
var fn = function(num) {
return new Promise(function(resolve, reject) {
if (typeof num == ‘number‘) {
resolve(num);
} else {
reject(‘TypeError‘);
}
})
}
fn(2).then(function(num) {
console.log(‘first: ‘ + num);
return num + 1;
})
.then(function(num) {
console.log(‘second: ‘ + num);
return num + 1;
})
.then(function(num) {
console.log(‘third: ‘ + num);
return num + 1;
});
// 输出结果
first: 2
second: 3
third: 4
即
new一个Promise对象时是立即执行其中的执行函数,但是then方法与catch方法中的参数函数是异步执行的。
即运行代码的过程中遇到then方法和catch方法,需要挂起其对应的参数函数,直到执行栈为空,才执行微任务队列中的任务。(顺带提一句,直至当前的微任务队列清空,下一个宏任务才能进栈)
Promise.all([Promise.resolve(4),5,new Promise((resolve,reject)=>resolve(8))]).then(val=>console.log(val))
切记,参数为一个数组,都由Promise对象组成。且传递给then的参数是所有Promise对象resolve的值组成的数组。如果参数中有一个Promise对象失败的话,则将第一个失败的reject值传递给catch。
Promise.race([Promise.resolve(4),5,new Promise((resolve,reject)=>resolve(8))]).then(val=>console.log(val))
同样的,参数是一个由Promise对象组成的数组,传递给then的值是第一个完成的Promise中resolve的值。
但是,Promise这个异步方案还不够完美,我们异步的最终目标就是以同步的书写方式书写异步。
酷吧!就是说我们并不关心他是不是异步,不管是同步还是异步我们都以从上到下的书写方式来书写代码,这才是人类的思维方式啊!
所以也就产生了async这个异步解决方案了。
需要注意的是,async是基于Promise的,即他不是一套独立的解决方案,而是基于Promise的异步机制的一次进步,代替then()的写法。
先声明几个重要的知识点:
async function async1(){
console.log(‘async1 start‘)
await async2()
console.log(‘async1 end‘)
}
async function async2(){
console.log(‘async2‘)
}
console.log(‘script start‘)
setTimeout(function(){
console.log(‘setTimeout‘)
},0)
async1();
new Promise(function(resolve){
console.log(‘promise1‘)
resolve();
}).then(function(){
console.log(‘promise2‘)
})
console.log(‘script end‘)
执行顺序是这样的:
tips:
标签:erro 表达式 多个 进入 a+b 网络 epc 引用类型 面向
原文地址:https://www.cnblogs.com/chargeworld/p/12045266.html