二进制浮点数中0.1和0.2并不精确,它们相加的结果并非刚好等于0.3,所以结果为false,可以设置一个误差范围值,通常称为机器精度,对于javaScript来说,通常是2^-52
从ES6开始,该值定义为Number.EPSILON
1 | function numberCloseEnoughToEqual(n1,n2){ |
二进制浮点数中0.1和0.2并不精确,它们相加的结果并非刚好等于0.3,所以结果为false,可以设置一个误差范围值,通常称为机器精度,对于javaScript来说,通常是2^-52
从ES6开始,该值定义为Number.EPSILON
1 | function numberCloseEnoughToEqual(n1,n2){ |
1 | async function sleep(delay){ |
promise没有按顺序执行,但是await按顺序接收到每个promise的值
1 | async function randomDelay(id){ |
1 | async function randomDelay(id){ |
只有传入的所有Promise都完成,返回promise才能完成,如果有任何promise被拒绝,返回的主promise就立即拒绝(抛弃任何其他promise的结果),如果全部完成,你会得到一个数组,其中包含传入的所有promise的完成值,对于拒绝的情况,你只会的到第一个拒绝promise的拒绝理由值,这种模式成为门,所有人到齐了才开门,传入空数组会立即完成
1 | Promise.all=function(promises){ |
例:
1 | function runAsync (x) { |
只有第一个决议的promise取胜,并且决议结果成为返回promise的决议。其他任务虽然会继续进行,但是race已经不管那些任务的结果了,不能传入空数组,传入空数组promise.race()会挂住,且永远不会决议
1 | function runAsync (x) { |
1 | Promise.race=function(promises){ |
1 | Promsie.resolve=function(param){ |
1 | Promise.reject=function(reason){ |
1 | Promsie.prototype.catch=function(onRejected){ |
1 | Promise.prototype.finally=function(cb){ |
1 | const arr=[1,2,3]; |
一秒后打印出1,2,3
1 | arr.reduce( |
1 | function red(){ |
mergepromise
1 | const time = (timer) => { |
异步加载图片
1 | function loadImage(url){ |
1 | function loadImage(url){ |
1 | function Foo(who){ |
1 | Foo={ |
1 | class Foo{ |
本文转载自(
[]: https://blog.csdn.net/cc18868876837/article/details/81211729)
文章目录
前言
_ _ proto _ _ 属性
prototype属性
constructor属性
总结
提示:不要排斥,静下心来,认真读完,你就搞懂了!(可以先看一下最后的总结部分再回过头来完整看完)
前言
作为一名前端工程师,必须搞懂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 | function Foo() {...}; |
以上代码表示创建一个构造函数Foo(),并用new关键字实例化该构造函数得到一个实例化对象f1。这里稍微补充一下new操作符将函数作为构造器进行调用时的过程:函数被调用,然后新创建一个对象,并且成了函数的上下文(也就是此时函数内部的this是指向该新创建的对象,这意味着我们可以在构造器函数内部通过this参数初始化值),最后返回该新对象的引用,详细请看:详解JavaScript中的new操作符。虽然是简简单单的两行代码,然而它们背后的关系却是错综复杂的,如下图所示:
看到这图别怕,让我们一步步剖析,彻底搞懂它们!
图的说明:右下角为图例,红色箭头表示__proto__属性指向、绿色箭头表示prototype属性的指向、棕色实线箭头表示本身具有的constructor属性的指向,棕色虚线箭头表示继承而来的constructor属性的指向;蓝色方块表示对象,浅绿色方块表示函数(这里为了更好看清,Foo()仅代表是函数,并不是指执行函数Foo后得到的结果,图中的其他函数同理)。图的中间部分即为它们之间的联系,图的最左边即为例子代码。
__ proto __ 属性
首先,我们需要牢记两点:
①____proto____和constructor属性是对象所独有的;② prototype属性是函数所独有的。但是由于JS中函数也是一种对象,所以函数也拥有__proto__和constructor属性,这点是致使我们产生困惑的很大原因之一。上图有点复杂,我们把它按照属性分别拆开,然后进行分析:
第一,这里我们仅留下 proto 属性,它是对象所独有的,可以看到__proto__属性都是由一个对象指向一个对象,即指向它们的原型对象(也可以理解为父对象),那么这个属性的作用是什么呢?它的作用就是当访问一个对象的属性时,如果该对象内部不存在这个属性,那么就会去它的__proto__属性所指向的那个对象(可以理解为父对象)里找,如果父对象也不存在这个属性,则继续往父对象的__proto__属性所指向的那个对象(可以理解为爷爷对象)里找,如果还没找到,则继续往上找…直到原型链顶端null(可以理解为原始人。。。),再往上找就相当于在null上取值,会报错(可以理解为,再往上就已经不是“人”的范畴了,找不到了,到此结束,null为原型链的终点),由以上这种通过__proto__属性来连接对象直到null的一条链即为我们所谓的原型链。
其实我们平时调用的字符串方法、数组方法、对象方法、函数方法等都是靠__proto__继承而来的。
prototype属性,别忘了一点,就是我们前面提到要牢记的两点中的第二点,它是函数所独有的,(除了Object基本类型),它是从一个函数指向一个对象。它的含义是函数的原型对象,也就是这个函数(其实所有函数都可以作为构造函数)所创建的实例的原型对象,由此可知:f1.proto === Foo.prototype,它们两个完全一样。那prototype属性的作用又是什么呢?它的作用就是包含可以由特定类型的所有实例共享的属性和方法,也就是让该函数所实例化的对象们都可以找到公用的属性和方法。任何函数在创建的时候,其实会默认同时创建该函数的prototype对象。
感谢网友的指出,这里解释一下上段中“每个对象都有构造函数”这句话。这里的意思是每个对象都可以找到其对应的constructor,因为创建对象的前提是需要有constructor,而这个constructor可能是对象自己本身显式定义的或者通过proto在原型链中找到的。而单从constructor这个属性来讲,只有prototype对象才有。每个函数在创建的时候,JS会同时创建一个该函数对应的prototype对象,而函数创建的对象.proto === 该函数.prototype,该函数.prototype.constructor===该函数本身,故通过函数创建的对象即使自己没有constructor属性,它也能通过__proto__找到对应的constructor,所以任何对象最终都可以找到其构造函数(null如果当成对象的话,将null除外)。如下:
我们需要牢记两点:
①__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 | function inherit(Child, Parent) { |
方法名constructor会告诉解释器在使用new操作符创建类的新实例时,应该调用这个函数,构造函数的定义不是必须的,不定义构造函数相当于将构造函数定义为空函数
1 | class Person{ |
默认情况下,类构造函数会在执行后返回this对象,构造函数返回的对象会被用作实例化的对象,如果没用引用新创建的this对象,那么这个对象会被销毁。不过,如果,返回的不是this对象,而是其他对象,那么这个对象不会通过instanceof操作符检测出跟类有关联。
1 | class Person{ |
调用类构造函数必须使用new操作符,而普通构造函数如果不使用new调用,那么就会以全局的this作为内部对象,调用类构造函数没有使用new则会抛出错误
1 | let p3=Person();//TypeError |
类构造函数在实例化后,可以在实例上引用它
1 | class Person{} |
1 | class Person{} |
类标识符有prototype属性,而这个原型也有一个constructor属性指向类自身
1 | class Person{ |
使用instanceof检查一个对象和类构造函数,以确定对象是不是类的实例。
1 | let p=new Person(); |
类本身具有和普通构造函数一样的行为,在类的上下文中,类本身使用new调用时就被当做构造函数,类中的constructor方法不会被当做构造函数,
1 | class Person{ |
每次通过new调用类标识符时都会执行类构造函数,可以为新创建的实例this添加自有属性。每个实例都对应一个唯一的成员对象,所有成员都不会在原型上共享
1 | class Person{ |
为了在实例间共享方法,类定义语法在类块中定义的方法作为原型方法
1 | //在类块中定义的所有内容都会定义在类的原型上 |
可以把方法定义在类构造函数或者类块中,但不能在类块给原型添加原始值或对象作为成员数据,但是可以在类块外部手动添加成员数据
1 | Person.greeting="My name is" |
类方法等同于对象属性因此可以使用字符串,符号或计算的值作为键,
也支持获取和设置访问器
1 | const symbolKey=Symbol('symbolkey'); |
这些方法通常用于执行不特定于实例的操作,也不要求存在类的实例
与原型成员类似,静态成员每个类上只能有一个
静态类成员在类定义中使用static关键字作为前缀,在静态成员中,this引用类自身,其他所有约定跟原型成员一样,静态类方法适合作为实例工厂
1 | class Person{ |
[]: https://www.bookstack.cn/read/es6-3rd/spilt.1.docs-class-extends.md
[]: https://www.bookstack.cn/read/es6-3rd/spilt.3.docs-class-extends.md
通过new.target保存通过new关键字调用的类或函数,在普通函数中调用new.target返回undefined,通过在实例化时检测new.target是不是抽象基类,可以阻止对抽象基类的实例化
1 | class Vehicle{ |
1 | class SuperArray extends Array{ |
script元素包含async属性和defer属性:
async:可选,表示应该立即开始下载脚本,但不能阻止其他页面动作,比如下载资源或等待其他脚本加载,只对外部脚本文件有效,标记过async的脚本并不保证能按照它们出现的次序执行
defer:可选,表示脚本可以延迟到文档完全被解析和渲染后再执行,相当于告诉浏览器立即下载,延迟执行,且按照顺序执行
只对外部脚本有效,在IE7及更早版本中,对行内脚本也可以指定这个属性
如果把script标签放在head标签里,意味着必须把所有JavaScript代码都下载,加息和解释完成后,才能渲染压面页面在浏览器解析到body的其实标签时开始渲染。因此现代Web通常把js引用放在body元素中页面内容后面这样页面就会在处理js代码之前完全渲染页面
综合了原型链和盗用构造函数,将两者优点结合,使用原型链继承原型上的属性和方法,通过盗用构造函数继承实例属性,这样既可以把方法定义在原型上实现重用,又可以让每个实例都有自己的属性
1 | function SuperType(name){ |
1 | //object.create() |
object()函数创建一个临时构造函数,将传入对象赋值给这个构造函数原型,然后返回这个临时类型的一个实例。本质上,object()是对传入的对象执行了一次浅复制,anotherperson.proto=person,如果person是构造函数,则anotherperson的[[Prototype]]将没有指向的prototype对象,也就无法通过prototype对象找到constructor进而使用construcor上面的方法和属性
这里的object函数相当与object.create()
1 |
|
原型式继承适合不需要单独创建构造函数,但仍然需要在对象间共享信息的场合。
与原型式继承比较接近的一种继承方式是寄生式继承
1 | function createAnother(original){ |
寄生式继承同样适合主要关注对象,而不在乎类型和构造函数的场景。痛过寄生式继承给对象添加函数会导致函数难以重用,与构造函数模式类似。
1 | function inheritPrototype(subType,superType){//参数分别是子类和父类的构造函数 |
另一种写法:
1 | function SuperType(name){ |
寄生式组合继承基本模式:
只调用一次SuperType构造函数,效率最高
执行上下文有全局上下文和函数上下文,块级上下文:
上下文中的代码在执行时,会创建变量对象的一个作用域链,这个作用域链决定了各级上下文中的代码在访问变量和函数时的顺序,代码正在执行的上下文的变量对象始终位于作用域链的最前端,如果上下文时函数,则其活动对象用作变量对象,作用域链中的下一个变量对象来自包含上下文…以此类推至全局上下文,全局上下文变量对象始终是作用域链的最后一个变量对象(window)
执行上下文主要有全局上下文和函数上下文两种(eval()调用内部存在第三种上下文),但有其他方式增强作用域链,
try/catch语句中的catch块
with语句
这两种情况会在作用域链的前端添加一个变量对象,对with语句来说,会向作用域链前端添加指定对象,对catch语句,会创建一个新的变量对象,这个变量对象会包含要抛出的错误对象的声明
1 | function buildUrl(){ |
with语句将location对象作为上下文,因此location会被添加到作用域链的前端
ECMAScript变量可以包含两种不同类型的数据:原始值和引用值,原始值就是最简单的数据:Undefined,Null,Boolean,Number,String,Symbol,保存原始值的变量是按值访问的,因为我们操作的就是存储在变量中的实际值。
引用值是保存在内存中的对象,JavaScript不允许直接访问内存位置,操作对象时,实际上操作的对该对象的引用,而非实际的对象本身,因此,保存引用值的变量是按引用访问的。
引用值可以随时添加,修改删除属性。
而原始值不能有属性,原始值的初始化只使用原始字面量形式,如果使用new关键字则JavaScript会创建一个Object类型的实例,
1 | let name1="Nicholas"; |
通过变量把一个原始值赋值到另一个变量时,原始值会被复制得到新变量的位置。
1 | let num1=5; |
num1和num2相互独立,num2是num1的副本
当把引用值从一个变量赋值给另一个变量时,存储在变量中的值也会被复制到新变量所在位置,区别在于:这里复制的值时一个指针,它指向存储在堆内存中的对象。两个变量实际上指向同一个对象。
1 | let obj1=new Object(); |
ECMAScript中所有函数的参数都是按值传递,这意味着函数外的值会被复制到函数内部参数中,就像一个变量赋值到另一个变量一样,如果是原始值,那么就跟原始值变量的复制一样,如果是引用值就和引用值的变量复制一样。
1 | function addTen(num){//count的值被复制到参数num,函数内部num+10但不会影响函数外部的原始变量count |
如果是对象:
1 | function setName(obj){ |
typeof用来判断一个变量是否为原始类型,即判断一个变量是否为字符串,数字,布尔值或undefined的最好方式,值为对象null,那么typeof返回Object
typeof虽然对原始值很有用,但是对引用值用处不大,我们通常关心一个值是不是对象,而是想知道它是什么类型的对象。
如果变量是给定引用类型的实例,则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 | let name1="Nicholas"; |
通过变量把一个原始值赋值到另一个变量时,原始值会被复制得到新变量的位置。
1 | let num1=5; |
num1和num2相互独立,num2是num1的副本
当把引用值从一个变量赋值给另一个变量时,存储在变量中的值也会被复制到新变量所在位置,区别在于:这里复制的值时一个指针,它指向存储在堆内存中的对象。两个变量实际上指向同一个对象。
1 | let obj1=new Object(); |
ECMAScript中所有函数的参数都是按值传递,这意味着函数外的值会被复制到函数内部参数中,就像一个变量赋值到另一个变量一样,如果是原始值,那么就跟原始值变量的复制一样,如果是引用值就和引用值的变量复制一样。
1 | function addTen(num){//count的值被复制到参数num,函数内部num+10但不会影响函数外部的原始变量count |
如果是对象:
1 | function setName(obj){ |
typeof用来判断一个变量是否为原始类型,即判断一个变量是否为字符串,数字,布尔值或undefined的最好方式,值为对象null,那么typeof返回Object
typeof虽然对原始值很有用,但是对引用值用处不大,我们通常关心一个值是不是对象,而是想知道它是什么类型的对象。
如果变量是给定引用类型的实例,则instanceof操作符返回true
1 | console.log(person instanceof Object);//变量person是Object吗 |
按照定义所有引用值都是Object的实例,因此通过instanceof检测的任何引用值和Object构造函数都会返回true。如果,使用instanceof检测原始值,则始终会返回false,因为原始值不是对象。
ES6之前:
1 | console.log(+0===-0);//true |
ES6中的object.is()与===很像,但同时考虑了上述边界情形,这个方法接收两个参数:
1 | console.log(Object.is(true,1));//false |
检查超过两个值是否相等
1 | function recursiveEqual(x,...rest){ |
constructor两个作用,
1 | console.log((2).constructor === Number); // true |
1 | function Fn(){}; |
Object.prototype.toString.call()使用Object对象的原型方法toString来判断数据类型
1 | var a = Object.prototype.toString; |
注意:同样是检测对象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方法。