0%

二进制浮点数中0.1和0.2并不精确,它们相加的结果并非刚好等于0.3,所以结果为false,可以设置一个误差范围值,通常称为机器精度,对于javaScript来说,通常是2^-52

从ES6开始,该值定义为Number.EPSILON

1
2
3
4
5
6
function numberCloseEnoughToEqual(n1,n2){
return Math.abs(n1-n2)<Number.EPSILON
}
var a=0.1+0.2;
var b=0.3;
console.log(numberCloseEnoughToEqual(a,b));

实现sleep

1
2
3
4
5
6
7
8
9
async function sleep(delay){
return new Promise((resolve)=>setTimeout(resolve,delay));
}
async function foo(){
const t0=Date.now();
await sleep(1500);//暂停1500ms,await会期望等到一个实现thenable接口的对象,没有的话也会将其包装为promise
console.log(Date.now()-t0);

}

实现平行加速

promise没有按顺序执行,但是await按顺序接收到每个promise的值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
async function randomDelay(id){
const delay=Math.random()*1000;
return new Promise((resolve)=>setTimeout(()=>{
console.log(`${id} finished`);
resolve()
},delay))
}
async function foo(){
const t0=Date.now()
const p0=randomDelay(0);
const p1=randomDelay(1);
const p2=randomDelay(2);
const p3=randomDelay(3);
await p0;
await p1;
await p2;
await p3;
setTimeout(console.log,0,`${Date.now()-t0} ms elapsed`)
}
foo();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
async function randomDelay(id){
const delay=Math.random()*1000;
return new Promise((resolve)=>setTimeout(()=>{
console.log(`${id} finished`);
resolve(id)
},delay))
}
async function foo(){
const t0=Date.now()
const promises=Array(5).fill(null).map((key,index)=>randomDelay(index));
for(const p of promises){
console.log(`awaited ${await p}`)
}
setTimeout(console.log,0,`${Date.now()-t0} ms elapsed`)
}
foo();

Promise.all原理

只有传入的所有Promise都完成,返回promise才能完成,如果有任何promise被拒绝,返回的主promise就立即拒绝(抛弃任何其他promise的结果),如果全部完成,你会得到一个数组,其中包含传入的所有promise的完成值,对于拒绝的情况,你只会的到第一个拒绝promise的拒绝理由值,这种模式成为门,所有人到齐了才开门,传入空数组会立即完成

1
2
3
4
5
6
7
8
9
10
11
12
13
Promise.all=function(promises){
const res=[];
return new Promise((resolve,reject)=>{
promises.forEach(promise=>{
promise.then((value,index)=>{
res[idx]=value;//解析promise里面的值
if(res.length==promises.length){
resolve(res)
}
},reason=>reject(reason))
})
})
}

例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
 function runAsync (x) {
const p = new Promise(r => setTimeout(() => r(x, console.log(x)), 1000))
return p
}
function runReject (x) {
const p = new Promise((res, rej) => setTimeout(() => rej(`Error: ${x}`, console.log(x)), 1000 * x))
return p
}
Promise.all([runAsync(1), runReject(4), runAsync(3), runReject(2)])
.then(res => console.log(res))
.catch(err => console.log(err))
// 1s后输出
1
3
// 2s后输出
2
Error: 2
// 4s后输出
4

Promise.race():

只有第一个决议的promise取胜,并且决议结果成为返回promise的决议。其他任务虽然会继续进行,但是race已经不管那些任务的结果了,不能传入空数组,传入空数组promise.race()会挂住,且永远不会决议

1
2
3
4
5
6
7
8
9
10
11
function runAsync (x) {
const p = new Promise(r => setTimeout(() => r(x, console.log(x)), 1000))
return p
}
Promise.race([runAsync(1), runAsync(2), runAsync(3)])
.then(res => console.log('result: ', res))
.catch(err => console.log(err))
//1
'result: ' 1
2
3

Promise.race()原理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Promise.race=function(promises){
promises=Array.from(promises);
return new Promise((resolve,reject)=>{
if(promises.length===0){
return;//空数组挂起
}
else{
for(let i=0;i<promises.length;i++){
Promise.resolve(promises[i]).then((data)=>{
resolve(data);
return;//只会返回第一个先resolve的,其他resolve的race不管
},(err)=>{
reject(err);
return;
})
}
}

})
}

Promise.resolve()原理

1
2
3
4
5
6
7
8
9
10
11
12
13
Promsie.resolve=function(param){
if(param instanceof Promise)return param;//如果param本身是promise,则直接返回
return new Promise((resolve,reject)=>{
if(param &&typeof param==='object'&& typeof param.then==='function'){//如果param是个对象并且param是thenable
setTimeout(()=>{//则返回的promise会跟随这个thenable的对象,采用最终状态
param.then(resolve,reject);
})

}else{
resolve(param)//如果param是值,则直接以该值为成功状态返回
}
})
}

Promise.reject()

1
2
3
4
5
Promise.reject=function(reason){
return new Promise((resolve,reject)=>{
return reject(reason)
})
}

Promise.catch()

1
2
3
Promsie.prototype.catch=function(onRejected){
return this.then(null,onRejected);//catch后可以继续.then因此调用this.then后可以返回一个新的promise继续.then
}

Promise.finally():

1
2
3
4
5
6
7
8
9
10
11
Promise.prototype.finally=function(cb){
return this.then((value)=>{
return Promise.resolve(callback).then(()=>{
return value;//finally后可以继续.then因此调用this.then后可以返回一个新的promise继续.then
})
},(err)=>{
return Promise.resolve(callback).then(()=>{
throw err;
})
})
}

每隔一秒打印1,2,3

1
2
3
const arr=[1,2,3];
arr.reduce(
(p,x)=>p.then(()=>new Promise(resolve=>setTimeout(()=>resolve(console.log(x)),1000))),Promise.resolve())

一秒后打印出1,2,3

1
2
3
arr.reduce(
(p,x)=>p.then(new Promise(resolve=>setTimeout(resolve(console.log(x)),1000))),Promise.resolve()
)

红绿灯交替闪烁

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
function red(){
console.log("red")
}
function yellow(){
console.log("yellow")
}
function green(){
console.log("green")
}
const light=(time,cb)=>{
return new Promise(resolve=>{
setTimeout(()=>{
cb();
resolve();
},time)
})

}
const step=function(){
Promise.resolve()
.then(()=>{
return light(3000,red)
})
.then(()=>{
return light(2000,yellow)
})
.then(()=>{
return light(1000,green)
})}
step();

mergepromise

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
const time = (timer) => {
return new Promise(resolve => {
setTimeout(() => {
resolve()
}, timer)
})
}
const ajax1 = () => time(2000).then(() => {
console.log(1);
return 1
})
const ajax2 = () => time(1000).then(() => {
console.log(2);
return 2
})
const ajax3 = () => time(1000).then(() => {
console.log(3);
return 3
})

function mergePromise (promises) {
// 在这里写代码
let data=[]
let promise=Promise.resolve();
promises.forEach(ajax=>{
//第一次then是调用ajax,第二次then是解析ajax的结果
promise=promise.then(ajax).then(res=>{data.push(res);return data})//把每次结果返回
})
return promise;

}

mergePromise([ajax1, ajax2, ajax3]).then(data => {
console.log("done");
console.log(data); // data 为 [1, 2, 3]
});

异步加载图片

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
function loadImage(url){
return new Promise((resolve,reject)=>{
const img=new Image();
img.onload=function(){
console.log("一张图片加载完成")
resolve(img)
}
img.onerror=function(){
reject(new Error("Cannot load"+url));
}
img.src=url;
})
}
var urls = [
"https://hexo-blog-1256114407.cos.ap-shenzhen-fsi.myqcloud.com/AboutMe-painting1.png",
"https://hexo-blog-1256114407.cos.ap-shenzhen-fsi.myqcloud.com/AboutMe-painting2.png",
"https://hexo-blog-1256114407.cos.ap-shenzhen-fsi.myqcloud.com/AboutMe-painting3.png",
"https://hexo-blog-1256114407.cos.ap-shenzhen-fsi.myqcloud.com/AboutMe-painting4.png",
"https://hexo-blog-1256114407.cos.ap-shenzhen-fsi.myqcloud.com/AboutMe-painting5.png",
"https://hexo-blog-1256114407.cos.ap-shenzhen-fsi.myqcloud.com/bpmn6.png",
"https://hexo-blog-1256114407.cos.ap-shenzhen-fsi.myqcloud.com/bpmn7.png",
"https://hexo-blog-1256114407.cos.ap-shenzhen-fsi.myqcloud.com/bpmn8.png",
];
function limitedLoad(urls,handler,limit){
let sequence=[...urls];
let promises=sequence.splice(0,limit).map((url,index)=>{
return handler(url).then(index=>{
return index;//调用loadImage返回最先加载完成的那张照片下标
})
})
return sequence.reduce(
(p,url)=>{
return p.then(()=>{return Promise.race(promises)})//得到第一张加载完成的图片的下标
.then(fastIndex=>{promises[fastIndex]=handler(url).then(()=>{return fastIndex})})//将已经加载完成的照片的位置替换为为加载的照片
.catch(err=>{console.error(err)})
},Promise.resolve())//初始为Promise.resolve()
.then(()=>{return Promise.all(promises)})//最后3个用Promise.all加载


}
limitedLoad(urls,loadImage,3)
.then(res=>{
console.log("图片全部加载完成")
console.log(res)
})
.catch(err=>{
console.error(err)
})

判断图片是否加载延迟

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
function loadImage(url){
return new Promise((resolve,reject)=>{
const img=new Image();
img.onload=function(){
console.log("一张图片加载完成")
resolve(img)
}
img.onerror=function(){
reject(new Error("Cannot load"+url));
}
img.src=url;
})
}
function timeout(){
var p=new promise((resolve,reject)=>{
setTimeout(()=>{
reject('图片超时')
},5000)
})
}
Promise.race([loadImage(),timeout()]).then((data)=>{
console.log(data);
}).catch((err)=>{
console.log(err)
})

面向对象的原型模式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function Foo(who){
this.me=who
}
Foo.prototype.identify=function(){
return "I am "+this.me
}
function Bar(who){
Foo.call(this,who)
}
Bar.prototype=Object.create(Foo.prototype);
Bar.prototype.speak=function(){
alert("Hello"+this.identify()+".")
}
var b1=new Bar("b1");
var b2=new Bar("b2");
b1.speak();
b2.speak();

原型模式

对象关联的委托模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Foo={
init:function(who){this.me=who},
identify:function(){return "I am "+this.me}
}
Bar=Object.create(Foo);
Bar.speak=function(){
alert("Hello"+this.identify()+".")
}
var b1=Object.create(Bar)
b1.init("b1");
var b2=Object.create(Bar)
b2.init("b2");
b1.speak();
b2.speak();

委托模式

类设计模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Foo{
constructor(who){
var me=who

}
identify(){
return "I am "+this.me
}
}
class Bar extends Foo{
constructor(who){
super();//这里super指的是父类的构造函数,相当于Foo.prototype.constructor.call(this);this指的是子类Bar
this.me=who
}
speak(){
alert("hello"+super.identify())//这里super作为对象,指的是父类的原型对象,相当于Foo.prototype
}
}
var b1=new Bar('b1');
b1.speak();
var b2=new Bar('b2');
b2.speak();

本文转载自(

[]: https://blog.csdn.net/cc18868876837/article/details/81211729)

文章目录

  1. 前言

  2. _ _ proto _ _ 属性

  3. prototype属性

  4. constructor属性

  5. 总结
    提示:不要排斥,静下心来,认真读完,你就搞懂了!(可以先看一下最后的总结部分再回过头来完整看完)

  6. 前言
      作为一名前端工程师,必须搞懂JS中的prototype、_proto__与constructor属性,相信很多初学者对这些属性存在许多困惑,容易把它们混淆,本文旨在帮助大家理清它们之间的关系并彻底搞懂它们。这里说明一点,__proto__属性的两边是各由两个下划线构成(这里为了方便大家看清,在两下划线之间加入了一个空格: proto _,读作“dunder proto”,“double underscore proto”的缩写),实际上,该属性在ES标准定义中的名字应该是[[Prototype]],具体实现是由浏览器代理自己实现,谷歌浏览器的实现就是将[[Prototype]]命名为__proto__,大家清楚这个标准定义与具体实现的区别即可(名字有所差异,功能是一样的),可以通过该方式检测引擎是否支持这个属性:Object.getPrototypeOf({proto: null}) === null。本文基于谷歌浏览器(版本 72.0.3626.121)的实验结果所得。
             现在正式开始! 让我们从如下一个简单的例子展开讨论,并配以相关的图帮助理解:

1
2
3
4
function Foo() {...};
let f1 = new Foo();


以上代码表示创建一个构造函数Foo(),并用new关键字实例化该构造函数得到一个实例化对象f1。这里稍微补充一下new操作符将函数作为构造器进行调用时的过程:函数被调用,然后新创建一个对象,并且成了函数的上下文(也就是此时函数内部的this是指向该新创建的对象,这意味着我们可以在构造器函数内部通过this参数初始化值),最后返回该新对象的引用,详细请看:详解JavaScript中的new操作符。虽然是简简单单的两行代码,然而它们背后的关系却是错综复杂的,如下图所示:

1

看到这图别怕,让我们一步步剖析,彻底搞懂它们!
  图的说明:右下角为图例,红色箭头表示__proto__属性指向、绿色箭头表示prototype属性的指向、棕色实线箭头表示本身具有的constructor属性的指向,棕色虚线箭头表示继承而来的constructor属性的指向;蓝色方块表示对象,浅绿色方块表示函数(这里为了更好看清,Foo()仅代表是函数,并不是指执行函数Foo后得到的结果,图中的其他函数同理)。图的中间部分即为它们之间的联系,图的最左边即为例子代码。

  1. __ proto __ 属性
    首先,我们需要牢记两点:

    ①____proto____和constructor属性是对象所独有的;② prototype属性是函数所独有的。但是由于JS中函数也是一种对象,所以函数也拥有__proto__和constructor属性,这点是致使我们产生困惑的很大原因之一。上图有点复杂,我们把它按照属性分别拆开,然后进行分析:

    2

  第一,这里我们仅留下 proto 属性,它是对象所独有的,可以看到__proto__属性都是由一个对象指向一个对象,即指向它们的原型对象(也可以理解为父对象),那么这个属性的作用是什么呢?它的作用就是当访问一个对象的属性时,如果该对象内部不存在这个属性,那么就会去它的__proto__属性所指向的那个对象(可以理解为父对象)里找,如果父对象也不存在这个属性,则继续往父对象的__proto__属性所指向的那个对象(可以理解为爷爷对象)里找,如果还没找到,则继续往上找…直到原型链顶端null(可以理解为原始人。。。),再往上找就相当于在null上取值,会报错(可以理解为,再往上就已经不是“人”的范畴了,找不到了,到此结束,null为原型链的终点),由以上这种通过__proto__属性来连接对象直到null的一条链即为我们所谓的原型链。
  其实我们平时调用的字符串方法、数组方法、对象方法、函数方法等都是靠__proto__继承而来的。

  1. prototype属性
      第二,接下来我们看 prototype 属性:

  3
  
    prototype属性,别忘了一点,就是我们前面提到要牢记的两点中的第二点,它是函数所独有的,(除了Object基本类型),它是从一个函数指向一个对象。它的含义是函数的原型对象,也就是这个函数(其实所有函数都可以作为构造函数)所创建的实例的原型对象,由此可知:f1.proto === Foo.prototype,它们两个完全一样。那prototype属性的作用又是什么呢?它的作用就是包含可以由特定类型的所有实例共享的属性和方法,也就是让该函数所实例化的对象们都可以找到公用的属性和方法。任何函数在创建的时候,其实会默认同时创建该函数的prototype对象。   

  1. constructor属性
      最后,我们来看一下 constructor 属性:
            constructor属性也是对象才拥有的,它是从一个对象指向一个函数,含义就是指向该对象的构造函数,每个对象都有构造函数(本身拥有或继承而来,继承而来的要结合__proto__属性查看会更清楚点,如下图所示),从上图中可以看出Function这个对象比较特殊,它的构造函数就是它自己(因为Function可以看成是一个函数,也可以是一个对象),所有函数和对象最终都是由Function构造函数得来,所以constructor属性的终点就是Function这个函数。

  感谢网友的指出,这里解释一下上段中“每个对象都有构造函数”这句话。这里的意思是每个对象都可以找到其对应的constructor,因为创建对象的前提是需要有constructor,而这个constructor可能是对象自己本身显式定义的或者通过proto在原型链中找到的。而单从constructor这个属性来讲,只有prototype对象才有。每个函数在创建的时候,JS会同时创建一个该函数对应的prototype对象,而函数创建的对象.proto === 该函数.prototype,该函数.prototype.constructor===该函数本身,故通过函数创建的对象即使自己没有constructor属性,它也能通过__proto__找到对应的constructor,所以任何对象最终都可以找到其构造函数(null如果当成对象的话,将null除外)。如下:

  1. 总结
       总结一下:

我们需要牢记两点:

①__proto__和constructor属性是对象所独有的;

② prototype属性是函数所独有的,因为函数也是一种对象,所以函数也拥有__proto__和constructor属性。
__proto__属性的作用就是当访问一个对象的属性时,如果该对象内部不存在这个属性,那么就会去它的__proto__属性所指向的那个对象(父对象)里找,一直找,直到__proto__属性的终点null,再往上找就相当于在null上取值,会报错。通过__proto__属性将对象连接起来的这条链路即我们所谓的原型链。
prototype属性的作用就是让该函数所实例化的对象们都可以找到公用的属性和方法,即f1.proto === Foo.prototype。
constructor属性的含义就是指向该对象的构造函数,所有函数(此时看成对象了)最终的构造函数都指向Function。
  本文就此结束了,希望对那些对JS中的prototype、__proto__与constructor属性有困惑的同学有所帮助。

最后,感谢这两篇博文,本文中的部分内容参考自这两篇博文:

一张图理解prototype、proto和constructor的三角关系
prototype和__proto__的关系是什么?
小彩蛋:实现继承(相对完美、优雅)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function inherit(Child, Parent) {
// 继承原型上的属性
Child.prototype = Object.create(Parent.prototype)
// 修复 constructor
Child.prototype.constructor = Child
// 存储超类
Child.super = Parent
// 静态属性继承
if (Object.setPrototypeOf) {
// setPrototypeOf es6
Object.setPrototypeOf(Child, Parent)
} else if (Child.__proto__) {
// __proto__ es6 引入,但是部分浏览器早已支持
Child.__proto__ = Parent
} else {
// 兼容 IE10 等陈旧浏览器
// 将 Parent 上的静态属性和方法拷贝一份到 Child 上,不会覆盖 Child 上的方法
for (var k in Parent) {
if (Parent.hasOwnProperty(k) && !(k in Child)) {
Child[k] = Parent[k]
}
}
}
}

类构造函数

方法名constructor会告诉解释器在使用new操作符创建类的新实例时,应该调用这个函数,构造函数的定义不是必须的,不定义构造函数相当于将构造函数定义为空函数

使用new调用类的构造函数会执行如下操作:

  • 在内存中创建一个新对象
  • 这个新对象内部的[[Prototype]]即__proto__指针被赋值为构造函数的prototype属性
  • 构造函数内部的this被赋值为这个新对象(即this指向新对象)
  • 执行构造函数内部的代码(给新对象添加属性)
  • 如果构造函数返回非空对象,则返回该对象;否则,返回刚创建的新对象
1
2
3
4
5
class Person{

}
let p1=new Person();
console.log(p1.__proto__===Person.prototype);//true

默认情况下,类构造函数会在执行后返回this对象,构造函数返回的对象会被用作实例化的对象,如果没用引用新创建的this对象,那么这个对象会被销毁。不过,如果,返回的不是this对象,而是其他对象,那么这个对象不会通过instanceof操作符检测出跟类有关联。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Person{
constructor(override){
this.foo="foo";
if(override){
return {
bar:"bar"
}
}
}
}
let p1=new Person(),p2=new Person(true);
console.log(p1)
console.log(p1 instanceof Person);
console.log(p2);//没有引用构造函数创建的this对象,对象被销毁而返回的其他对象与类没有关系
console.log(p2 instanceof Person)

类构造函数与普通构造函数区别

调用类构造函数必须使用new操作符,而普通构造函数如果不使用new调用,那么就会以全局的this作为内部对象,调用类构造函数没有使用new则会抛出错误

1
let p3=Person();//TypeError

类构造函数在实例化后,可以在实例上引用它

1
2
3
4
class Person{}
let p1=new Person();
//使用对类构造函数的引用创建一个新实例
let p2=new p1.constructor();

把类当成特殊的函数

1
2
3
class Person{}
console.log(Person);//class Person{}
console.log(typeof Person)//function

类标识符有prototype属性,而这个原型也有一个constructor属性指向类自身

1
2
3
4
class Person{

}
console.log(Person===Person.prototype.constructor);//true

使用instanceof检查一个对象和类构造函数,以确定对象是不是类的实例。

1
2
let p=new Person();
console.log(p instanceof Person)//true

类本身具有和普通构造函数一样的行为,在类的上下文中,类本身使用new调用时就被当做构造函数,类中的constructor方法不会被当做构造函数,

1
2
3
4
5
6
7
8
9
10
11
12
class Person{

}
let p1=new Person();
console.log(p1 instanceof Person);//true
console.log(p1.constructor=== Person);//true
console.log(p1 instanceof Person.constructor)//false
let p2=new Person.constructor();
console.log(p2 instanceof Person)//false
console.log(p2 .constructor=== Person);//false
console.log(p2 instanceof Person.constructor)//true

实例,原型和类成员

实例成员

每次通过new调用类标识符时都会执行类构造函数,可以为新创建的实例this添加自有属性。每个实例都对应一个唯一的成员对象,所有成员都不会在原型上共享

1
2
3
4
5
6
7
8
9
10
11
12
class Person{
constructor(){
this.name=new String('Jack');
this.sayName=()=>console.log(this.name);
}
sayAge(age){
console.log(age)
}
}
let p1=new Person(),p2=new Person();
console.log(p1.name===p2.name)//false
console.log(p1.sayAge(18)===p2.sayAge(19))//true

原型方法

为了在实例间共享方法,类定义语法在类块中定义的方法作为原型方法

1
2
 //在类块中定义的所有内容都会定义在类的原型上
console.log(p1.sayAge===Person.prototype.sayAge);//true

可以把方法定义在类构造函数或者类块中,但不能在类块给原型添加原始值或对象作为成员数据,但是可以在类块外部手动添加成员数据

1
Person.greeting="My name is"

类方法等同于对象属性因此可以使用字符串,符号或计算的值作为键,

也支持获取和设置访问器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
const symbolKey=Symbol('symbolkey');
class Person(){
stringKey(){
console.log('invoked stringKey')
}
[symbolKey](){
console.log('invoked symbolKey')
}
['computed'+'Key'](){
console.log('invoked computedKey')
}
set name(newvalue){
this.name_=newName;
}
get name(){
return this.name_
}

}
let p=new Person()
p.stringKey();
p[symbolKey]();
p.computedKey();
p.name='jake';
console.log(p.name);//Jake

静态类方法

这些方法通常用于执行不特定于实例的操作,也不要求存在类的实例

与原型成员类似,静态成员每个类上只能有一个

静态类成员在类定义中使用static关键字作为前缀,在静态成员中,this引用类自身,其他所有约定跟原型成员一样,静态类方法适合作为实例工厂

1
2
3
4
5
6
7
8
9
10
11
12
13
class Person{
constructor(){
this.name=new String('Jack');
this.sayName=()=>console.log(this.name);
}
sayAge(age){
console.log(age)
}
static create(){
return new Person(Math.floor(Math.random()*100))
}
}
console.log(Person.create())//Person {name: String, sayName: ƒ}name: String {'Jack'}sayName: ()=>console.log(this.name)[[Prototype]]: Object

关于继承

[]: https://www.bookstack.cn/read/es6-3rd/spilt.1.docs-class-extends.md

关于super

[]: https://www.bookstack.cn/read/es6-3rd/spilt.3.docs-class-extends.md

抽象基类

通过new.target保存通过new关键字调用的类或函数在普通函数中调用new.target返回undefined,通过在实例化时检测new.target是不是抽象基类,可以阻止对抽象基类的实例化

1
2
3
4
5
6
7
8
9
10
11
class Vehicle{
constructor(){
console.log(new.target);
if(new.target===Vehicle){
throw new Error('Vehicle cannot be directly instantiated')
}
}
}
class Bus extends Vehicle{}
new Bus();
new Vehicle()//testIframe.html:193 Uncaught Error: Vehicle cannot be directly instantiated

继承内置类型

1
2
3
4
5
6
7
8
9
10
11
class SuperArray extends Array{
shuffle(){
for(let i=this.length-1;i>=0;i--){
let j=Math.floor(Math.random(0,i+1));//0-i+1向下取整
[this[i],this[j]]=[this[j],this[i]]
}
}
}
let a=new SuperArray(1,2,3,4,5);
a.shuffle()
console.log(a);

script元素包含async属性和defer属性:

async:可选,表示应该立即开始下载脚本,但不能阻止其他页面动作,比如下载资源或等待其他脚本加载,只对外部脚本文件有效,标记过async的脚本并不保证能按照它们出现的次序执行

defer:可选,表示脚本可以延迟到文档完全被解析和渲染后再执行,相当于告诉浏览器立即下载,延迟执行,且按照顺序执行

只对外部脚本有效,在IE7及更早版本中,对行内脚本也可以指定这个属性

如果把script标签放在head标签里,意味着必须把所有JavaScript代码都下载,加息和解释完成后,才能渲染压面页面在浏览器解析到body的其实标签时开始渲染。因此现代Web通常把js引用放在body元素中页面内容后面这样页面就会在处理js代码之前完全渲染页面

组合继承

综合了原型链和盗用构造函数,将两者优点结合,使用原型链继承原型上的属性和方法,通过盗用构造函数继承实例属性,这样既可以把方法定义在原型上实现重用,又可以让每个实例都有自己的属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
function SuperType(name){
this.name=name;
this.colors=["red","blue","green"];
}
SuperType.prototype.sayName=function(){
console.log(this.name);//不能用箭头函数,因为箭头函数没有this绑定,会向上找到全局上下文
}
function SubType(name,age) {
SuperType.call(this,name);//SuperType的构造函数绑定SubType的构造函数,自定义实例属性,第二次调用SuperType
this.age=age;
}
//继承原型上的方法,让SubType的原型指向SuperType的实例,第一次调用SuperType,SubType的constructor会委托到SuperType.prototype上的constructor,进而指向SuperType
SubType.prototype=new SuperType();
SubType.prototype.sayAge=function(){
console.log(this.age);
}
let instance1=new SubType('Ann',29);
instance1.colors.push("balck");
console.log(instance1.colors);
instance1.sayName();
instance1.sayAge();

let instance2=new SubType("Greg",27);
console.log(instance2.colors);
instance2.sayName();
instance2.sayAge();

1

原型式继承

1
2
3
4
5
6
7
8
9
10
11
12
13
//object.create()
function object(o){
function F(){};//创建一个构造函数
F.prototype=o;//构造函数的原型指向参数对象o
return new F();//返回F对象实例
}
let person={
name:"Ann",
friends:["Mary","Harry","Van"]
}
let anotherPerson=object(person);
anotherPerson.friends.push("Mike");
console.log(person.friends);// ['Mary', 'Harry', 'Van', 'Mike']

object()函数创建一个临时构造函数,将传入对象赋值给这个构造函数原型,然后返回这个临时类型的一个实例。本质上,object()是对传入的对象执行了一次浅复制,anotherperson.proto=person,如果person是构造函数,则anotherperson的[[Prototype]]将没有指向的prototype对象,也就无法通过prototype对象找到constructor进而使用construcor上面的方法和属性

这里的object函数相当与object.create()

1
2
3
4
5
6
7
8

let person={
name:"Ann",
friends:["Mary","Harry","Van"]
}
let anotherPerson=object。create(person);
anotherPerson.friends.push("Mike");
console.log(person.friends);// ['Mary', 'Harry', 'Van', 'Mike']

原型式继承适合不需要单独创建构造函数,但仍然需要在对象间共享信息的场合。

寄生式继承

与原型式继承比较接近的一种继承方式是寄生式继承

1
2
3
4
5
6
7
8
9
10
11
12
13
function createAnother(original){
let clone=Object.create(original);//通过调用函数创建一个新对象
clone.sayHi=function(){//以某种方式增强这个对象
console.log("hi");
}
return clone;//返回这个对象
}
let person={
name:"Ann",
friends:["Mary","Harry","Van"]
}
let anotherPerson=createAnother(person);
anotherPerson.sayHi();

寄生式继承同样适合主要关注对象,而不在乎类型和构造函数的场景。痛过寄生式继承给对象添加函数会导致函数难以重用,与构造函数模式类似。

寄生式组合继承

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
function inheritPrototype(subType,superType){//参数分别是子类和父类的构造函数
let prototype=Object.create(superType.prototype);//先复制父类的原型对象
prototype.constructor=subType;//增强对象,让新的原型对象的构造函数指向子类构造函数
subType.prototype=prototype;//赋值对象,让子类原型指向新的原型
}
function SuperType(name){
this.name=name;
this.colors=["red","blue","green"];
}
SuperType.prototype.sayName=function(){
console.log(this.name);//不能用箭头函数,因为箭头函数没有this绑定,会向上找到全局上下文
}
function SubType(name,age) {
SuperType.call(this,name);//SuperType的构造函数绑定SubType的构造函数,调用Supertype()
this.age=age;
}
inheritPrototype(SubType,SuperType);
SubType.prototype.sayAge=function(){
console.log(this.age);
}
let instance1=new SubType('Ann',29);
instance1.colors.push("balck");
console.log(instance1.colors);
instance1.sayName();
instance1.sayAge();

另一种写法:

1
2
3
4
5
6
7
8
9
10
11
12
13
function SuperType(name){
this.name=name;
this.colors=["red","blue","green"];
}
SuperType.prototype.sayName=function(){
console.log(this.name);//不能用箭头函数,因为箭头函数没有this绑定,会向上找到全局上下文
}
function SubType(name,age) {
SuperType.call(this,name);//SuperType的构造函数绑定SubType的构造函数,调用Supertype()
this.age=age;
}
SubType.prototype=Object.create(SuperType.prototype)
SubType.prototype.constructor=Subtype

寄生式组合继承基本模式:

  • 先创建父类原型的一个副本prototype
  • 给prototype设置constructor属性为subType,增强对象,解决由于重写原型导致默认constructor丢失问题
  • 将新创建的对象赋值给子类型的原型

只调用一次SuperType构造函数,效率最高

执行上下文有全局上下文和函数上下文,块级上下文:

上下文中的代码在执行时,会创建变量对象的一个作用域链,这个作用域链决定了各级上下文中的代码在访问变量和函数时的顺序,代码正在执行的上下文的变量对象始终位于作用域链的最前端,如果上下文时函数,则其活动对象用作变量对象,作用域链中的下一个变量对象来自包含上下文…以此类推至全局上下文,全局上下文变量对象始终是作用域链的最后一个变量对象(window)

作用域链增强

执行上下文主要有全局上下文和函数上下文两种(eval()调用内部存在第三种上下文),但有其他方式增强作用域链,

try/catch语句中的catch块

with语句

这两种情况会在作用域链的前端添加一个变量对象,对with语句来说,会向作用域链前端添加指定对象,对catch语句,会创建一个新的变量对象,这个变量对象会包含要抛出的错误对象的声明

1
2
3
4
5
6
7
function buildUrl(){
let qs="?debug=true";
with(location){
let url=href+qs;
}
return url;
}

with语句将location对象作为上下文,因此location会被添加到作用域链的前端

原始值和引用值

ECMAScript变量可以包含两种不同类型的数据:原始值和引用值,原始值就是最简单的数据:Undefined,Null,Boolean,Number,String,Symbol,保存原始值的变量是按值访问的,因为我们操作的就是存储在变量中的实际值。

引用值是保存在内存中的对象,JavaScript不允许直接访问内存位置,操作对象时,实际上操作的对该对象的引用,而非实际的对象本身,因此,保存引用值的变量是按引用访问的。

区别

动态属性

引用值可以随时添加,修改删除属性。

而原始值不能有属性,原始值的初始化只使用原始字面量形式,如果使用new关键字则JavaScript会创建一个Object类型的实例,

1
2
3
4
5
6
7
8
let name1="Nicholas";
let name2=new String("Matt");
name1.age=27;
name2.age=26;
console.log(name1.age);//undefined
console.log(name2.age);//26
console.log(typeof name1);//string
console.log(typeof name2);//object

复制值

通过变量把一个原始值赋值到另一个变量时,原始值会被复制得到新变量的位置。

1
2
let num1=5;
let num2=num1;

num1和num2相互独立,num2是num1的副本

当把引用值从一个变量赋值给另一个变量时,存储在变量中的值也会被复制到新变量所在位置,区别在于:这里复制的值时一个指针,它指向存储在堆内存中的对象。两个变量实际上指向同一个对象。

1
2
3
4
let obj1=new Object();
let obj2=obj1;
obj1.name="Nic";
console.log(obj2.name);//Nic

传递参数

ECMAScript中所有函数的参数都是按值传递,这意味着函数外的值会被复制到函数内部参数中,就像一个变量赋值到另一个变量一样,如果是原始值,那么就跟原始值变量的复制一样,如果是引用值就和引用值的变量复制一样。

1
2
3
4
5
6
7
8
9
function addTen(num){//count的值被复制到参数num,函数内部num+10但不会影响函数外部的原始变量count
num+=10;
return num;

}
let count=20;
let res=addTen(count);
console.log(count);//20,没有变化
console.log(res);//30

如果是对象:

1
2
3
4
5
6
7
8
function setName(obj){
obj.name="Nic"//此时多了一个obj指针指向person,将name改为Nic
obj=new Object();//又创建一个指针,指向内存其他位置
obj.name="Greg";//函数调用结束,obj被销毁
}
let person=new Object();
setName(person);
console.log(person.name);//'Nic'

typeof

typeof用来判断一个变量是否为原始类型,即判断一个变量是否为字符串,数字,布尔值或undefined的最好方式,值为对象null,那么typeof返回Object

typeof虽然对原始值很有用,但是对引用值用处不大,我们通常关心一个值是不是对象,而是想知道它是什么类型的对象。

instanceof

如果变量是给定引用类型的实例,则instanceof操作符返回true

1
console.log(person instanceof Object);//变量person是Object吗

按照定义所有引用值都是Object的实例,因此通过instanceof检测的任何引用值和Object构造函数都会返回true。如果,使用instanceof检测原始值,则始终会返回false,因为原始值不是对象。

原始值和引用值

ECMAScript变量可以包含两种不同类型的数据:原始值和引用值,原始值就是最简单的数据:Undefined,Null,Boolean,Number,String,Symbol,保存原始值的变量是按值访问的,因为我们操作的就是存储在变量中的实际值。

引用值是保存在内存中的对象,JavaScript不允许直接访问内存位置,操作对象时,实际上操作的对该对象的引用,而非实际的对象本身,因此,保存引用值的变量是按引用访问的。

区别

动态属性

引用值可以随时添加,修改删除属性。

而原始值不能有属性,原始值的初始化只使用原始字面量形式,如果使用new关键字则JavaScript会创建一个Object类型的实例,

1
2
3
4
5
6
7
8
let name1="Nicholas";
let name2=new String("Matt");
name1.age=27;
name2.age=26;
console.log(name1.age);//undefined
console.log(name2.age);//26
console.log(typeof name1);//string
console.log(typeof name2);//object

复制值

通过变量把一个原始值赋值到另一个变量时,原始值会被复制得到新变量的位置。

1
2
let num1=5;
let num2=num1;

num1和num2相互独立,num2是num1的副本

当把引用值从一个变量赋值给另一个变量时,存储在变量中的值也会被复制到新变量所在位置,区别在于:这里复制的值时一个指针,它指向存储在堆内存中的对象。两个变量实际上指向同一个对象。

1
2
3
4
let obj1=new Object();
let obj2=obj1;
obj1.name="Nic";
console.log(obj2.name);//Nic

传递参数

ECMAScript中所有函数的参数都是按值传递,这意味着函数外的值会被复制到函数内部参数中,就像一个变量赋值到另一个变量一样,如果是原始值,那么就跟原始值变量的复制一样,如果是引用值就和引用值的变量复制一样。

1
2
3
4
5
6
7
8
9
function addTen(num){//count的值被复制到参数num,函数内部num+10但不会影响函数外部的原始变量count
num+=10;
return num;

}
let count=20;
let res=addTen(count);
console.log(count);//20,没有变化
console.log(res);//30

如果是对象:

1
2
3
4
5
6
7
8
function setName(obj){
obj.name="Nic"//此时多了一个obj指针指向person,将name改为Nic
obj=new Object();//又创建一个指针,指向内存其他位置
obj.name="Greg";//函数调用结束,obj被销毁
}
let person=new Object();
setName(person);
console.log(person.name);//'Nic'

typeof

typeof用来判断一个变量是否为原始类型,即判断一个变量是否为字符串,数字,布尔值或undefined的最好方式,值为对象null,那么typeof返回Object

typeof虽然对原始值很有用,但是对引用值用处不大,我们通常关心一个值是不是对象,而是想知道它是什么类型的对象。

instanceof

如果变量是给定引用类型的实例,则instanceof操作符返回true

1
console.log(person instanceof Object);//变量person是Object吗

按照定义所有引用值都是Object的实例,因此通过instanceof检测的任何引用值和Object构造函数都会返回true。如果,使用instanceof检测原始值,则始终会返回false,因为原始值不是对象。

对象标识和相等判定

ES6之前:

1
2
console.log(+0===-0);//true
cosole.log(NAN===NAN);//false

ES6中的object.is()与===很像,但同时考虑了上述边界情形,这个方法接收两个参数:

1
2
3
4
console.log(Object.is(true,1));//false
console.log(Object.is({},{}));//false
console.log(Object.is(+0,-0));//false
console.log(Object.is(NAN,NAN));//true

检查超过两个值是否相等

1
2
3
4
function recursiveEqual(x,...rest){
return Object.is(x,rest[0])||(rest.length<2||recursiveEqual(...rest));

}

constructor:

constructor两个作用,

  • 判断数据的类型
  • 对象实例通过prototype对象上的constructor访问它的构造函数,因此如果改变prototype对象,constructor就不能用来判断数据类型
1
2
3
4
5
6
console.log((2).constructor === Number); // true
console.log((true).constructor === Boolean); // true
console.log(('str').constructor === String); // true
console.log(([]).constructor === Array); // true
console.log((function() {}).constructor === Function); // true
console.log(({}).constructor === Object); // true
1
2
3
4
5
6
7
8
function Fn(){};

Fn.prototype = new Array();

var f = new Fn();

console.log(f.constructor===Fn); // false
console.log(f.constructor===Array); // true

Object.prototype.toString.call()

Object.prototype.toString.call()使用Object对象的原型方法toString来判断数据类型

1
2
3
4
5
6
7
8
9
10
var a = Object.prototype.toString;

console.log(a.call(2));
console.log(a.call(true));
console.log(a.call('str'));
console.log(a.call([]));
console.log(a.call(function(){}));
console.log(a.call({}));
console.log(a.call(undefined));
console.log(a.call(null));

注意:同样是检测对象obj调用toString方法,obj.toString()的结果和Object.prototype.toString.call(obj)的结果不一样,这是因为toString是Object的原型方法,而Array,function等类型作为Object的实例,都重写了toString()方法,不同的对象类型调用toString方法时,根据原型链的知识,调用的是对应的重写之后的toString()方法,(function类型返回内容为函数体的字符串,Array类型返回元素组成的字符串…),而不会去调用Object上原型toString方法(返回对象的具体类型),所以采用obj.toString()不能得到其对象类型,只能将obj转换为字符串类型;因此,在想要得到对象的具体类型时,应该调用Object原型上的toString方法。