charlesgc / dream Goto Github PK
View Code? Open in Web Editor NEW这是一个写博客的地方,主要是想把平时学习的内容汇集起来,做一个备忘录。除了这些,还会写一些其他的内容,比如关于人生,思考,以及看的书籍等等。 有没有兴趣随意star。
这是一个写博客的地方,主要是想把平时学习的内容汇集起来,做一个备忘录。除了这些,还会写一些其他的内容,比如关于人生,思考,以及看的书籍等等。 有没有兴趣随意star。
function Person () { }
let person1 = new Person()
person1.name = 'Charles'
person1.age = 27
person1.job = 'Software Engineer'
person1.sayName = function() {
console.log(this.name)
}
person1.sayName() // Charles
在这个例子中,Person就是一个构造函数,我们使用 new 创建了一个实例对象person1,这是是用构造函数创建对象,很简单吧。
我们创建的每个函数都有一个prototype属性,这个属性是一个指针,指向一个对象,这个对象就是我们通过调用构造函数创建的那个对象实例的原型对象。
这个原型对象就管理着由特定类型的创建的所有实例共享的属性和方法。也就是说我们通过某个构造函数创建对象实例,它都会从它指向的原型那里继承属性和方法。
由上个例子:
我们在浏览器打印一下,可以看到,Person.prototype是一个对象,这个原型对象上有一个constructor属性指向构造函数Person,如图所示,这里是一个循环引用。
我们现在知道了构造函数上有一个prototype属性指向了原型对象,原型对象上的constructor属性指向构造函数,那另一个属性__proto__属性呢?它是代表什么意思?有什么作用呢?
我们来看下面这个例子:
我们可以看到,用Man构造函数实例化的child里面也有一个__proto__参数,而且它同样指向一个对象,我们发现它和Man函数的原型对象一模一样,__proto__是每个实例上都有的属性,但prototype是构造函数上的属性,这两个不一样,但 p.__proto__和 Parent.prototype 却指向同一个对象。
我们从上面知道,构造函数和原型对象之间通过prototype和constructor进行连接,所以构造函数,实例和原型对象之间的关系如下图:
现在我们知道可以通过__proto__访问到对象的内部 [[Prototype]]
[[Prototype]] 是对象的一个内部属性,外部代码无法直接访问。
遵循 ECMAScript 标准,someObject.[[Prototype]] 符号用于指向 someObject 的原型。
__proto__属性在 ES6 时才被标准化,以确保 Web 浏览器的兼容性,但是不推荐使用,除了标准化的原因之外还有性能问题。为了更好的支持,推荐使用 Object.getPrototypeOf()。
通过改变一个对象的[[Prototype]]属性来改变和继承属性会对性能造成非常严重的影响,并且性能消耗的时间也不是简单的花费在obj.proto= ... 语句上, 它还会影响到所有继承自该[[Prototype]]的对象,如果你关心性能,你就不应该修改一个对象的 [[Prototype]]。
如果要读取或修改对象的 [[Prototype]] 属性,建议使用如下方案,但是此时设置对象的 [[Prototype]] 依旧是一个缓慢的操作,如果性能是一个问题,就要避免这种操作。
// 获取
Object.getPrototypeOf()
Reflect.getPrototypeOf()
// 修改
Object.setPrototypeOf()
Reflect.setPrototypeOf()
如果确要新建一个对象并继承另一个对象的[[prototype]],可以使用Object.create()。
var man = {
age: 50
};
var child = Object.create(man);
这时child上有一个指针__proto__ 指向man对象。
ECMAScript 中描述了原型链的概念,并将原型链作为实现继承的主要方法。其基本**是利用原型让一个引用类型继承另一个引用类型的属性和方法。简单回顾一下构造函数、原型和实例的关系:每个构造函数都有一个原型对象,原型对象都包含一个指向构造函数的指针,而实例都包含一个指向原型对象的内部指针。那么,假如我们让原型对象等于另一个类型的实例,结果会怎么样呢?显然,此时的原型对象将包含一个指向另一个原型的指针,相应地,另一个原型中也包含着一个指向另一个构造函数的指针。假如另一个原型又是另一个类型的实例,那么上述关系依然成立,如此层层递进,就构成了实例与原型的链条。这就是所谓原型链的基本概念。
来看下面这个例子:
function Parent(age) {this.age = 1}
function Son () {}
Son.prototype = new Parent()
var grandSon = new Son()
console.log(grandSon.age) // 1
为什么grandSon上并没有定义age属性,却能打印出 1 ,因为原型链的搜索机制是,当以读取模式访问一个实例属性时,首先会在实例中搜索该属性。如果没有找到该属性,则会继续搜索实例的原型。在通过原型链实现继承的情况下,搜索过程就得以沿着原型链继续向上。就拿上面的例子来说,调用
grandSon.age 会经历三个搜索步骤:1)搜索实例;2)搜索 Son.prototype ;3)搜索 Parent.prototype ,最后一步才会找到该属性。在找不到属性或方法的情况下,搜索过程总是要一环一环地前行到原型链末端才会停下来。
继续上面的例子验证我们的想法:
function Parent(age) {this.age = 1}
function Son () {}
Son.prototype = new Parent()
var grandSon = new Son()
console.log(grandSon.age) // 1
console.log(grandSon.__proto__ === Son.prototype) // true
console.log(grandSon.__proto__.__proto__ === Parent.prototype) // true
console.log(grandSon.__proto__.__proto__.__proto__ === Object.prototype) // true
console.log(grandSon.__proto__.__proto__.__proto__.__proto__ === null) // true
《JavaScript 高级程序设计第三版》
HTTP是一种能够获取如 HTML 这样的网络资源的 protocol(通讯协议)。它是在 Web 上进行数据交换的基础,是一种 client-server 协议,也就是说,请求通常是由像浏览器这样的接受方发起的。一个完整的Web文档通常是由不同的子文档拼接而成的,像是文本、布局描述、图片、视频、脚本等等。
HTTP被设计于20世纪90年代初期,是一种可扩展的协议。它是应用层的协议,通过TCP,或者是TLS-加密的TCP连接来发送,理论上任何可靠的传输协议都可以使用。因为其良好的扩展性,时至今日,它不仅被用来传输超文本文档,还用来传输图片、视频或者向服务器发送如HTML表单这样的信息。HTTP还可以根据网页需求,仅获取部分Web文档内容更新网页。
注意,HTTP本质是无状态的,使用Cookies可以创建有状态的会话。
明文传输(不加密),内容可能被窃听。
明文传输(不加密),内容可能被窃听。协议里的报文不使用二进制数据,而是文本形式
无法验证报文的完整性,内容可能被篡改。
无法验证报文的完整性,内容可能被篡改。这里说的完整性也就是指信息的准确度 因为接收方或者发送方没有办法确认对方发送过来的数据在中间有没有被篡改
不验证通信方的身份,有可能遭遇伪装。
不验证通信方的身份,有可能遭遇伪装。因为HTTP协议中不会对通信方进行确认 任何人都可以发送请求,而且服务器它对收到的请求也不会进行确认,只要收到了请求就会返回一个响应(当然这个只是在发送端的IP地址或者端口号没被Web服务器设定限制访问的前提下)
无状态,它是缺点也是优点吧,分不同的场景。
队头阻塞。
GET: 请求指定的页面信息,并返回实体主体,幂等操作
HEAD: 类似于get请求,只不过返回的响应中没有具体的内容,用于获取报头,幂等操作
POST: 向指定资源提交数据进行处理请求(例如提交表单或者上传文件),数据被包含在请求体中,非幂等操作
PUT : 从客户端向服务器传送的数据取代指定的文档的内容,幂等操作
PATCH: 对资源进行局部更新,非幂等操作
DELETE: 请求服务器删除指定的页面,幂等操作
CONNECT: CONNECT方法建立一个到由目标资源标识的服务器的隧道
TRACE: 追踪请求,查询发出去的请求是怎样被加工/篡改的,幂等操作。容易引发XST跨站追踪攻击。
OPTIONS: 查询服务器端支持的HTTP方法种类(幂等操作):
请求 | OPTIONS * HTTP/1.1 Host: lindaidai.wang |
---|---|
响应 | HTTP/1.1 200 OK Allow: GET, POST, HEAD, OPTIONS (返回服务器支持的方法) |
当服务端收到不支持的方法时,会返回 405 Method Not Allowed
,并且会把所有支持的方法写入响应报文首部字段Allow
中返回。
HTTP 响应状态代码指示特定 HTTP 请求是否已成功完成。响应分为五类:信息响应(100–199),成功响应(200–299),重定向(300–399),客户端错误(400–499)和服务器错误 (500–599)。
「请求已经接收到,需要进一步处理才能完成,但是HTTP/1.0 不支持。」
100 Continue
: 这个临时响应表明,迄今为止的所有内容都是可行的,客户端应该继续请求,如果已经完成,则忽略它。
101 Switching Protocols
:在HTTP升级为WebSocket时,如果服务器同意变更,则返回 101。
102 Processing (WebDAV)
: 此代码表示服务器已收到并正在处理该请求,但没有响应可用
103 Early Hints
: 此状态代码主要用于与Link 链接头一起使用,以允许用户代理在服务器仍在准备响应时开始预加载资源。
「成功处理请求。」
200 OK
:请求成功,通常返回的数据中带有响应体。
201 Created
: 该请求已成功,并因此创建了一个新的资源。这通常是在POST请求,或是某些PUT请求之后返回的响应。
202 Accepted
: 请求已经接收到,但还未响应,没有结果。意味着不会有一个异步的响应去表明当前请求的结果,预期另外的进程和服务去处理请求,或者批处理。
203 Non-Authoritative Information
: 服务器已成功处理了请求,但返回的实体头部元信息不是在原始服务器上有效的确定集合,而是来自本地或者第三方的拷贝。当前的信息可能是原始版本的子集或者超集。例如,包含资源的元数据可能导致原始服务器知道元信息的超集。使用此状态码不是必须的,而且只有在响应不使用此状态码便会返回200 OK的情况下才是合适的。
204 No Content
:服务器成功处理了请求,但不需要返回任何实体内容,并且希望返回更新了的元信息。响应可能通过实体头部的形式,返回新的或更新后的元信息。如果存在这些头部信息,则应当与所请求的变量相呼应。如果客户端是浏览器的话,那么用户浏览器应保留发送了该请求的页面,而不产生任何文档视图上的变化,即使按照规范新的或更新后的元信息应当被应用到用户浏览器活动视图中的文档。由于204响应被禁止包含任何消息体,因此它始终以消息头后的第一个空行结尾。
205 Reset Content
:服务器成功处理了请求,且没有返回任何内容。但是与204响应不同,返回此状态码的响应要求请求者重置文档视图。该响应主要是被用于接受用户输入后,立即重置表单,以便用户能够轻松地开始另一次输入。与204响应一样,该响应也被禁止包含任何消息体,且以消息头后的第一个空行结束。
206 Partial Content
:客户端进行了范围请求且服务端正常处理,响应报文的首部应该还有Content-Range
字段指定实体的范围。使用场景为HTTP分块下载和断点续传。
207 Multi-Status (WebDAV)
: 由WebDAV(RFC 2518)扩展的状态码,代表之后的消息体将是一个XML消息,并且可能依照之前子请求数量的不同,包含一系列独立的响应代码。
208 Already Reported (WebDAV)
: 在 DAV 里面使用: propstat 响应元素以避免重复枚举多个绑定的内部成员到同一个集合。
226 IM Used (HTTP Delta encoding)
: 服务器已经完成了对资源的 GET 请求,并且响应是对当前实例应用的一个或多个实例操作结果的表示。
「重定向状态,资源位置发生变动,需要重新请求。」
300 Multiple Choice
:被请求的资源有一系列可供选择的回馈信息,每个都有自己特定的地址和浏览器驱动的商议信息。用户或浏览器能够自行选择一个首选的地址进行重定向
301 Moved Permanently
:永久重定向,最新的URI为响应报文首部的 Location
字段。场景是:例如你的网站换了地址了,之前的地址不用了,若用户还是从之前的地址进的话则会返301
且在Location
中带上最新的URI。且浏览器默认会做缓存优化,减少服务器压力,在第二次访问的时候自动访问重定向的那个地址。
302 Found
:临时重定向,和301
不同,它表示请求的资源临时被移动到了别的URI上,因为是暂时的,所以不会被缓存。
303 See Other
:临时重定向,请求的资源临时被移动到了别的URI上,但是明确表示客户端应该使用GET方法获取资源。
304 Not Modefied
:客户端带条件请求时虽未满足条件但是也允许返回该资源,它虽然被划分在3xx
中,但其实和重定向没有关系。场景例如:协商缓存成功就会返回304 Not Modefied
,表示请求的资源在服务器上并未发送改变,告诉请求者可以使用缓存。
307 Temprary Redirect
:临时重定向,但是比302
更加明确,重定向的请求方法和实体都不允许变动。场景例如:HSTS
协议,强制客户端使用https
建立连接,比如你的网站从HTTP
升级到了HTTPS
,而你还是通过http://xxx
访问的话,就会返回307 Internal Redirect
。
308 Permanent Redirect
: 这意味着资源现在永久位于由 Location: HTTP Response 标头指定的另一个 URI。 这与 301 Moved Permanently HTTP
响应代码具有相同的语义,但用户代理不能更改所使用的 HTTP 方法:如果在第一个请求中使用 POST,则必须在第二个请求中使用 POST。
三种临时重定向简单比较:
- `302 Found`,基本的临时重定向
- `303 See Other`,明确表示客户端应该使用`GET`方法
- `307 Temprary Redirect`,请求方法和实体都不允许变动
「客户端出现错误。」
400 Bad Request
:请求报文中存在语法错误,但是没有具体指出是哪里
401 Unauthorized
:需要有通过HTTP认证的认证信息或者表示用户认证失败
403 Forbidden
:请求资源被拒绝,原因是:比如法律禁止、信息敏感
404 Not Found
:请求资源未找到,表示没在服务器上找到相应的资源
405 Method Not Allowed
: 请求行中指定的请求方法不能被用于请求相应的资源。该响应必须返回一个Allow 头信息用以表示出当前资源能够接受的请求方法的列表。 鉴于 PUT,DELETE 方法会对服务器上的资源进行写操作,因而绝大部分的网页服务器都不支持或者在默认配置下不允许上述请求方法,对于此类请求均会返回405错误。
406 Not Acceptable
:请求的资源的内容特性无法满足请求头中的条件,因而无法生成响应实体。
407 Proxy Authentication Required
: 与401响应类似,只不过客户端必须在代理服务器上进行身份验证。代理服务器必须返回一个 Proxy-Authenticate 用以进行身份询问。客户端可以返回一个 Proxy-Authorization 信息头用以验证。
408 Request Timeout
: 请求超时。客户端没有在服务器预备等待的时间内完成一个请求的发送。客户端可以随时再次提交这一请求而无需进行任何更改。
409 Conflict
: 由于和被请求的资源的当前状态之间存在冲突,请求无法完成。这个代码只允许用在这样的情况下才能被使用:用户被认为能够解决冲突,并且会重新提交新的请求。该响应应当包含足够的信息以便用户发现冲突的源头。
410 Gone
: 被请求的资源在服务器上已经不再可用,而且没有任何已知的转发地址。这样的状况应当被认为是永久性的。如果可能,拥有链接编辑功能的客户端应当在获得用户许可后删除所有指向这个地址的引用。如果服务器不知道或者无法确定这个状况是否是永久的,那么就应该使用 404 状态码。除非额外说明,否则这个响应是可缓存的。
411 Length Required
: 服务器拒绝在没有定义 Content-Length 头的情况下接受请求。在添加了表明请求消息体长度的有效 Content-Length 头之后,客户端可以再次提交该请求。
412 Precondition Failed
: 服务器在验证在请求的头字段中给出先决条件时,没能满足其中的一个或多个。这个状态码允许客户端在获取资源时在请求的元信息(请求头字段数据)中设置先决条件,以此避免该请求方法被应用到其希望的内容以外的资源上。
413 Payload Too Large
: 服务器拒绝处理当前请求,因为该请求提交的实体数据大小超过了服务器愿意或者能够处理的范围。此种情况下,服务器可以关闭连接以免客户端继续发送此请求。如果这个状况是临时的,服务器应当返回一个 Retry-After 的响应头,以告知客户端可以在多少时间以后重新尝试。
414 URI Too Long
: 请求的URI 长度超过了服务器能够解释的长度,因此服务器拒绝对该请求提供服务。这比较少见,通常的情况包括:本应使用POST方法的表单提交变成了GET方法,导致查询字符串(Query String)过长。
415 Unsupported Media Type
: 对于当前请求的方法和所请求的资源,请求中提交的实体并不是服务器中所支持的格式,因此请求被拒绝。
416 Range Not Satisfiable
: 如果请求中包含了 Range 请求头,并且 Range 中指定的任何数据范围都与当前资源的可用范围不重合,同时请求中又没有定义 If-Range 请求头,那么服务器就应当返回416状态码。
417 Expectation Failed
: 此响应代码意味着服务器无法满足 Expect 请求标头字段指示的期望值。
418 I'm a teapot
: 服务器拒绝尝试用 “茶壶冲泡咖啡”。
421 Misdirected Request
: 该请求针对的是无法产生响应的服务器。 这可以由服务器发送,该服务器未配置为针对包含在请求 URI 中的方案和权限的组合产生响应。
422 Unprocessable Entity (WebDAV)
: 请求格式良好,但由于语义错误而无法遵循。
423 Locked (WebDAV)
: 正在访问的资源被锁定。
424 Failed Dependency (WebDAV)
: 由于先前的请求失败,所以此次请求失败。
425 Too Early
: 服务器不愿意冒着风险去处理可能重播的请求。
426 Upgrade Required
: 服务器拒绝使用当前协议执行请求,但可能在客户机升级到其他协议后愿意这样做。 服务器在 426 响应中发送 Upgrade 头以指示所需的协议。
428 Precondition Required
: 原始服务器要求该请求是有条件的。 旨在防止“丢失更新”问题,即客户端获取资源状态,修改该状态并将其返回服务器,同时第三方修改服务器上的状态,从而导致冲突。
429 Too Many Requests
: 用户在给定的时间内发送了太多请求(“限制请求速率”)。
431 Request Header Fields Too Large
: 服务器不愿意处理请求,因为它的 请求头字段太大( Request Header Fields Too Large)。 请求可以在减小请求头字段的大小后重新提交。
451 Unavailable For Legal Reasons
: 用户请求非法资源,例如:由政府审查的网页。
「服务端出现错误。」
500 Internal Server Error
:服务器内部错误,但是没有具体指出是哪里,和400
有点像。
501 Not Implemented
:表示客户端请求的功能还不支持
502 Bad GateWay
:服务器自身是正常的,但是代理服务器无法获取到合法响应(点外卖时外卖小哥没送)
503 Service Unavailable
:服务器内部处于超负载状态或进行停机维护(就像是本店今天不开张)
504 Gateway Timeout
: 当服务器作为网关,不能及时得到响应时返回此错误代码。
505 HTTP Version Not Supported
: 服务器不支持请求中所使用的HTTP协议版本。
506 Variant Also Negotiates
: 服务器有一个内部配置错误:对请求的透明内容协商导致循环引用。
507 Insufficient Storage
: 服务器有内部配置错误:所选的变体资源被配置为参与透明内容协商本身,因此不是协商过程中的适当端点。
508 Loop Detected (WebDAV)
: 服务器在处理请求时检测到无限循环。
510 Not Extended
: 客户端需要对请求进一步扩展,服务器才能实现它。服务器会回复客户端发出扩展请求所需的所有信息。
511 Network Authentication Required
: 511 状态码指示客户端需要进行身份验证才能获得网络访问权限
在HTTP工作开始之前,客户端首先要通过网络与服务器建立连接,该连接是通过 TCP 来完成的,该协议与 IP 协议共同构建 Internet,即著名的 TCP/IP 协议族,因此 Internet 又被称作是 TCP/IP 网络。HTTP 是比 TCP 更高层次的应用层协议,根据规则,只有低层协议建立之后,才能进行高层协议的连接,因此,首先要建立 TCP 连接,一般 TCP 连接的端口号是80;
一旦建立了TCP连接,客户端就会向服务器发送请求命令;
例如:GET/sample/hello.jsp HTTP/1.1
客户端发送其请求命令之后,还要以头信息的形式向服务器发送一些别的信息,之后客户端发送了一空白行来通知服务器,它已经结束了该头信息的发送;
客户端向服务器发出请求后,服务器会客户端返回响应;
例如: HTTP/1.1 200 OK
响应的第一部分是协议的版本号和响应状态码
正如客户端会随同请求发送关于自身的信息一样,服务器也会随同响应向用户发送关于它自己的数据及被请求的文档;
服务器向客户端发送头信息后,它会发送一个空白行来表示头信息的发送到此为结束,接着,它就以 Content-Type 响应头信息所描述的格式发送用户所请求的实际数据;
一般情况下,一旦服务器向客户端返回了请求数据,它就要关闭 TCP 连接,然后如果客户端或者服务器在其头信息加入了这行代码 Connection:keep-alive
,TCP 连接在发送后将仍然保持打开状态,于是,客户端可以继续通过相同的连接发送请求。保持连接节省了为每个请求建立新连接所需的时间,还节约了网络带宽。
下一篇:将分析HTTP报文结构
引用类型是一种数据结构,它在别的语言中被称为类,但是在 JavaScript 中实际上并没有类,虽然提供了很多类似“类”的语法(class 关键字),但它不具备传统的面向对象语言所支持的类和接口等基本结构。
let person = new Object()
console.dir(person)
这里通过 new 关键字来生成一个 Object 引用类型的实例,可以发现它的原型上有很多公有的方法,引用类型的实例也被成为引用类型的值,是一个对象。Object是Javascript中最基本的引用类型,其他的引用类型都是它的子类型,所以其他引用类型上会有Object的基本方法。
ECMAScript 原生的引用类型有:
接下来我会介绍几个比较常用的引用类型来详细分析。
Object 是最常见的引用类型,原生的引用类型都继承自 Object 类型,可以通过点表示法和方括号表示法来访问对象,前者书写更加简单,后者可以支持一些特殊语法和表达式,但是书写比较复杂(同时由于要解析表达式,性能也稍慢)。
// new 操作符
let person = new Object();
person.name = "Charles Guo"
person.age = 29
person['sex'] = "man"
person['a b'] = "meter"
console.log(person) // { name: 'Charles Guo', age: 29, sex: 'man', 'a b': 'meter' }
// 对象字面量
let person = {
name : "Charles Guo",
age : 29
};
可以使用 new 关键字来动态生成一个对象,另外它不仅限于普通的对象,还可以生成包装类型的对象。
let str = "abc"
let wrapStr = new Object("abc") // 等同于 new String("abc")
console.log(typeof str) // 'string'
console.log(typeof wrapStr) // 'object'
console.log(wrapStr instanceof String) // true 注意 String 首字母是大写,代表是 String 包装类型而非 string 基本类型
wrapStr 这个包装对象看上去和基本类型相似,但它是一个对象,进一步说是 String 包装类型的实例,这个我们放到之后讲。
除了 Object 之外, Array 类型基本是 ECMAScript 中最常用的类型了。创建数组的基本方式有两种。第一种是使用 Array 构造函数,如下面的代码所示。
let colors = new Array();
Array构造函数可以接受一个或者多个参数,当它接受一个参数时并且传递的是数值类型时,它会创建一个length为此数值的稀疏数组,当它接受其他类型的参数时,它会创建包含那些值的项的数组,如下代码所示:
let arr1 = new Array(20)
console.log(arr1) // [ <20 empty items> ]
console.log(arr1.length) // 20
let arr2 = new Array('apple', 'orange','pear')
console.log(arr2) // [ 'apple', 'orange', 'pear' ]
console.log(arr2.length) // 3
数组有一个 length 的内部属性,它是可以被修改的,可以利用这个特点快速清空数组和增加数组长度。数组的最后一项总是length - 1。
let arr3 = [1,2,3]
console.log(arr3[arr3.length - 1]) // 3
arr3.length = 0
console.log(arr3) // []
arr3.length = 100
console.log(arr3) //[ <100 empty items> ] 创建的都是空单元的稀疏数组,不推荐直接使用
创建数组的第二种基本方式是使用数组字面量表示法。数组字面量由一对包含数组项的方括号表示,多个数组项之间以逗号隔开。
let colors = ["red", "blue", "green"]; // 创建一个包含 3 个字符串的数组
let names = []; // 创建一个空数组
当尝试将 Array 类型转换为字符串时(这可能会存在于隐式转换中),默认会调用数组原型上的 toString 方法,它会依次调用数组中每个元素的 toString 方法 (null 和 undefined 是例外,它们会直接转为空字符串)。
let arr = [{a:1},123,()=>{},undefined]
console.log(arr.toString()) // "[object Object],123,()=>{}," 最后以逗号结尾,因为 undefined 变成空字符串了
调用数组的toLocaleString()、toString()和valueOf()方法,都会依次调用数组每一项的toLocaleString()、toString()和valueOf()方法,数组继承的 toLocaleString()、toString()和valueOf() 方法,在默认情况下都会以逗号分隔的字符串的形式返回数组项。数组的join()方法,可以接受一个参数,让我们自定义它返回的字符串的连接符。
let colors = ["red", "green", "blue"];
console.log(colors.join(",")); //red,green,blue
console.log(colors.join("||")); //red||green||blue
如果数组中的某一项的值是 null 或者 undefined ,那么该值在 join() 、toLocaleString() 、 toString() 和 valueOf() 方法返回的结果中以空字符串表示
数组的push()方法和pop方法可以让数组表现的像栈一样,栈是一种 LIFO(Last-In-First-Out,后进先出)的数据结构,也就是最新添加的项最早被移除。而栈中项的插入(叫做推入)和移除(叫做弹出),只发生在一个位置——栈的顶部,这里的栈顶就是数组的最后一个元素。
let arr4 = [1,2,3]
arr4.push(4)
console.log(arr4) //[ 1, 2, 3, 4 ]
arr4.pop()
console.log(arr4) //[ 1, 2, 3 ]
队列数据结构的访问规则是 FIFO(First-In-First-Out,先进先出)。队列在列表的末端添加项,从列表的前端移除项,使用数组的shift()方法和push()方法可以模仿队列。
let arr5 = ['a','b','c']
arr5.shift()
console.log(arr5) //[ 'b', 'c' ]
arr5.push('d')
console.log(arr5) //[ 'b', 'c', 'd' ]
与shift()相反,数组还有一个unshift(),它可以在数组前端添加任意项并返回数组的新长度。
let arr6 = [1,2,3]
arr6.unshift(0)
console.log(arr6) //[ 0, 1, 2, 3 ]
在默认情况下,sort() 方法按升序排列数组项——即最小的值位于最前面,最大的值排在最后面。为了实现排序,sort() 方法会调用每个数组项的 toString() 转型方法,然后比较得到的字符串,以确定如何排序。即使数组中的每一项都是数值, sort() 方法比较的也是字符串。
let values = [0, 1, 5, 10, 15];
values.sort();
console.log(values); //0,1,10,15,5
之所以会产生这样的结果,是因为sort()在比较时,会先比较所有字符串的第一位的编码,因为"5"的编码大于"1",所有会直接退出比较,就会产生"15" < "5"的结果。这样的结果明显不符合现实的逻辑,所以sort() 方法可以接收一个比较函数作为参数,以便我们指定哪个值位于哪个值的前面。
let arr = [1,2,10,20,100,200]
console.log(arr.sort((a,b) => a - b)) // [ 1, 2, 10, 20, 100, 200 ]
sort 是一个高阶函数,即支持传入一个函数作为参数,每次比较时都会调用传入的参数,其中参数 a 和 b 就是两个准备进行比较的字符串,在上一章还提到过,如果两个字符串相减会转为 Number 类型再进行计算,这样就可以避免字符串类型比较造成的问题。
同时如果传入的函数返回值大于 0 ,则 b 会排在 a 前面,小于 0 则 a 会排在 b 前面,等于 0 则不变,根据这个特点可以控制返回数组是顺序还是倒序。
let arr = [1,2,10,20,100,200]
// [ 200, 100, 20, 10, 2, 1 ] 倒序数组
// 第一次比较时 b 为 2,a 为 1,由于返回值大于 0,所以 b 将排在 a 的前面,变成 [2,1],以此类推
// 注意是插入排序
console.log(arr.sort((a,b) => b - a))
sort 它会修改原数组,而不是新生成一个数组作为返回值,使用前请考虑清楚是否需要对原数组进行拷贝。
let arr = [1,2,10,20,100,200]
console.log(arr.sort((a,b) => b - a)) // [ 200, 100, 20, 10, 2, 1 ]
console.log(arr) // [ 200, 100, 20, 10, 2, 1 ] 原数组被修改了!
上述作为参数的函数中,可以通过 Math.random 随机返回大于小于 0 的数字可以实现数组乱序,但是并不是真正的乱序,而洗牌算法可以解决这个问题.
concat 会将参数添加到数组末尾,不像 sort 会修改原数组,它会创建一个当前数组的副本(浅拷贝)。
let arr = [
1,
'abb',
{
name: 'marry',
age: 26
}
]
let arr1 = arr.concat()
arr1[2].age = 28
arr1[2].sex = "woman"
console.log(arr); // [ 1, 'abb', { name: 'marry', age: 28, sex: 'woman' } ]
console.log(arr1); // [ 1, 'abb', { name: 'marry', age: 28, sex: 'woman' } ]
另外如果参数包含数组,会给数组进行一层降维
let arr = [1, 2, 3]
console.log(arr.concat(4, [5, 6], [7, [8, 9]])) // [ 1, 2, 3, 4, 5, 6, 7, [ 8, 9 ] ]
console.log(arr) // [1,2,3]
当然现在更推荐使用 ES6 的扩展运算符,写法更加简洁,和 concat 实现的功能类似,同样也会浅拷贝数组。
let arr = [1, 2, 3]
console.log([...arr, 4, ...[5, 6], ...[7, [8, 9]]]) // [ 1, 2, 3, 4, 5, 6, 7, [ 8, 9 ] ]
console.log(arr) // [1,2,3]
slice 会基于参数来切割数组,它同样会浅拷贝数组,当传入不同参数会有不同功能
slice 方法可以将类数组转为真正的数组
let arr = [1, 2, 3, 4, 5]
console.log(arr.slice()) // [1, 2, 3, 4, 5] 浅拷贝原数组
console.log(arr.slice(2)) // [3, 4, 5]
console.log(arr.slice(2, 3)) // [3]
console.log(arr.slice(2, 1)) // []
console.log(arr.slice(2, -1)) // [3, 4] 等同于 arr.slice(2, -1 + 5)
console.log(Array.prototype.slice.call({0: "a", length: 1})) // ["a"]
splice 可以认为是 push, pop, unshift, shift 的结合,并且能够指定插入/删除的位置,非常强大,但它传入但参数也更为复杂,所以一般只有在操作数组具体下标元素的时候才会使用,同时它也会修改原数组,使用时请注意。
let colors = ["red", "green", "blue"];
let removed = colors.splice(0,1); // 删除第一项
console.log(colors); // green,blue
console.log(removed); // red,返回的数组中只包含一项
removed = colors.splice(1, 0, "yellow", "orange"); // 从位置 1 开始插入两项
console.log(colors); // green,yellow,orange,blue
console.log(removed); // 返回的是一个空数组
removed = colors.splice(1, 1, "red", "purple"); // 插入两项,删除一项
console.log(colors); // green,red,purple,orange,blue
console.log(removed); // yellow,返回的数组中只包含一项
这两个方法都接收两个参数:要查找的项和(可选的)表示查找起点位置的索引。其中, indexOf() 方法从数组的开头(位置 0)开始向后查找, lastIndexOf() 方法则从数组的末尾开始向前查找。这两个方法都返回要查找的项在数组中的位置,或者在没找到的情况下返回1。如果使用 indexOf 判断 NaN 是否在数组中,永远会返回 -1。
解决这个问题可以使用 ES6 的 includes 方法,它会返回一个布尔值,而非目标元素下标,同时它可以判断 NaN 是否存在与目标数组中。
let arr = [1,2,3,4,5,NaN]
console.log(arr.indexOf(NaN)) // -1
console.log(arr.includes(NaN)) // true
reverse 和 sort 以及 splice 一样会修改原数组
let arr = [1,2,3,4,5]
console.log(arr.reverse()) // [5,4,3,2,1]
console.log(arr) // [5,4,3,2,1]
ES5 为数组提供了 5 个迭代方法,它们都是高阶函数,第一个参数是一个函数,第二个参数是函数的 this 指向。
这里单独说一下map和forEach,从字面意思山看它们似乎很接近,MDN是这样解释它们的区别:
forEach() 为每个数组元素执行callback函数;不像 map() 或者 reduce(),它总是返回 undefined 值,并且不可链式调用。典型用例是在一个链的最后执行副作用。
let array1 = [1, 3, 5, 7]
let array2 = [2, 4, 6, 8]
const map1 = array1.map(x => x * 2);
const map2 = array2.forEach(x => x * 3);
console.log(map1); // [ 2, 6, 10, 14 ]
console.log(map2); // undefined
console.log(array1); // [ 1, 3, 5, 7 ]
console.log(array2); // [ 2, 4, 6, 8 ]
因为map生成一个新数组,当你不打算使用返回的新数组却使用map是违背设计初衷的,请用forEach或者for-of替代。
对一个空数组无论参数中的函数返回什么,调用 some 都会返回 false, 调用 every 都会返回 true。
let arr = []
console.log(arr.some(()=>{})) // false
console.log(arr.every(()=>{})) // true
reduce() 方法从数组的第一项开始,逐个遍历到最后。而 reduceRight() 则从数组的最后一项开始,向前遍历到第一项。
这两个方法都接收两个参数:一个在每一项上调用的函数和(可选的)作为归并基础的初始值。传给 reduce() 和 reduceRight() 的函数接收 4 个参数:前一个值、当前值、项的索引和数组对象。
reduce 也就是归并方法,个人认为是数组中最高级的使用方法,用的好可以实现一些非常强大的功能,这里举个的例子:多维数组扁平化
const flat = (arr) => {
return arr.reduce(function(prev, cur) {
return prev.concat(Array.isArray(cur) ? flat(cur) : cur)
}, [])
}
let a = [
[0, 1],
[2, 3],
[4, [5, 6, 7]]
]
console.log(flat(a)); [ 0, 1, 2, 3, 4, 5, 6, 7 ]
关于 reduce 还有一个关于下标的注意点,当 reduce 只传一个参数时,index 的下标是从 1 也就是数组第二个元素开始的(如果此时数组为空会报错),当 reduce 传入第二个参数,会作为遍历的起始值,此时 index 的下标就从 0 也就是数组第一个元素开始。
let arr = ["b", "c", "d", "e"]
arr.reduce((pre, cur, index) => {
console.log(index)
return pre + cur
})
// 1
// 2
// 3
arr.reduce((pre, cur, index) => {
console.log(index)
return pre + cur
}, "a")
// 0
// 1
// 2
// 3
正则表达式可以使用构造函数动态生成,也可以使用字面量快速生成,由于正则表达式实例也是对象,所以也会有属性,常用的属性有
let reg = /\[abc]/ig
console.dir(reg)
let reg2 = new RegExp('\\[abc]', "ig") // RegExp 第二个参数为正则标志
console.dir(reg2)
console.log(reg.test("[abc]"), reg2.test("[abc]"))
打印结果如下
reg 和 reg2 实现的功能是相同的,第一种更简便,第二种更灵活,需要结合实际情况灵活使用
另外还有一个比较重要的点,可以看到 2 个正则对象的 lastIndex 都为 5,此时如果继续用 test 方法匹配会返回 false。
let reg = /\[abc]/ig
console.dir(reg)
let reg2 = new RegExp('\\[abc]', "ig") // RegExp 第二个参数为正则标志
console.dir(reg2)
console.log(reg.test("[abc]"), reg2.test("[abc]"))
console.log(reg.test("[abc]"), reg2.test("[abc]"))
之所以出现这样的情况是因为正则对象内部保存的 lastIndex 属性会决定下次正则匹配的位置,第一次用 test 方法匹配成功后,lastIndex 从 0 变成 5,同时下次会从参数的第 6 个元素开始匹配,此时发现匹配不到任何元素,所以会返回 false ,并将 lastIndex 重置为默认值 0。
关于正则其实非常复杂,深入会涉及到状态机,回溯,贪婪匹配等知识点,写的不好会影响系统的性能,但是如果能搞懂其中的奥秘,写出优质的正则,能够很大程度上解放劳动力,例如给整个项目替换部分代码,另外 Vue 的模版字符串编译也是依赖正则的匹配来提取属性,自定义指令等。
函数也是对象,因此函数名实际上也是一个指向函数对象的指针。可以通过 new 关键字来动态生成一个函数。
let func = new Function("a","console.log(a)")
console.log(func(123)) // 123
Function 函数作为构造函数时,至少接受一个参数,当只传入一个参数时,会直接将参数作为函数体,当传入超过一个函数时,会将最后一个参数作为函数体,之前的所有参数会作为生成的函数的参数。
而一般情况我们是不需要使用这种方式的,直接使用字面量的形式创建函数,因为前者虽然更加灵活,但是性能并不是非常理想,同时还有可能存在安全隐患(类似 eval,可能会被恶意用户注入恶意代码运行,形成 XSS 攻击)
JavaScript 中的函数没有重载,但是可以实现一定程度上的参数重载。
import $ from 'jquery'
$("p").css("background-color"); // color
$("p").css("background-color",'red');
这里是一个 jquery 的代码,当调用 css 方法传入一个参数时,会返回 p 节点当前的背景颜色,如果传入 2 个参数,会设置背景颜色,css 方法的功能取决于传入参数的个数,原理是利用函数参数个数来判断返回属性还是设置属性。
解析器在向执行环境中加载数据时,对函数声明和函数表达式并非一视同仁。解析器会率先读取函数声明,并使其在执行任何代码之前可用(可以访问);至于函数表达式,则必须等到解析器执行到它所在的代码行,才会真正被解释执行。
func()
// 函数表达式
const func2 = function () {
console.log(2)
}
func2()
// 函数声明
function func() {
console.log(1)
}
执行顺序:func 声明 -> func 执行 -> func2 声明 -> func2 执行在进入全局环境之前,func 由于"函数声明提升"被提升到最前面执行,随后再是执行整个代码,所以上面代码并不会报错,fun2 是函数表达式,如果将 func2 执行的代码放到 func2 声明之前,就会发生错误,因为函数表达式不会被提升。
关于函数声明和函数表达式还有一些小细节:
var sum = function sum() {
//...
sum() // 在函数内部 sum 指向的是右边的词法名称,而非左边的 sum 变量
}
右边的 sum 会作为匿名函数的“词法名称”,这种情况常用于自身的递归,如果没有这个词法名称函数只能使用 arguments.callee 方法来实现递归(无法使用左边的 sum 变量),而函数内部的 arguments 对象由于性能问题已不推荐使用,所以如果有递归的需求,推荐给匿名函数添加一个词法名称,另外需要注意的是,词法名称是常量,函数内部无法修改。
关于这点可以看这题:
var b = 10;
(function b() {
// 内部作用域,会先去查找是有已有变量b的声明,有就直接赋值20,确实有了呀。发现了具名函数 function b(){},拿此b做赋值;
// IIFE的函数无法进行赋值(内部机制,类似const定义的常量),所以无效。
// (这里说的“内部机制”,想搞清楚,需要去查阅一些资料,弄明白IIFE在JS引擎的工作方式,堆栈存储IIFE的方式等)
b = 20;
console.log(b); // [Function b]
console.log(window.b); // 10,不是20
})();
函数还有一个 length 属性,它表示的是函数希望接受的形参个数。
形参的数量不包括剩余参数个数,仅包括第一个具有默认值之前的参数个数
function func(a,b,...c) {}
console.log(func.length) // 2
function func2(a = 1,b,c) {}
console.log(func2.length) // 0
function func3(a,b = 2,c) {}
console.log(func3.length) // 1
可以看到第一个函数的 length 属性为 2,因为后面是剩余参数,不计算在 length 长度中,而第二个因为参数 a 含有默认值,所以会返回 a 之前的参数,同时因为 a 之前没有参数所以最终返回 0,第三个例子中参数 b 有默认值,所以返回 b 之前的参数个数,也就是 1。
函数在运行时还会生成一个 this 对象,它可以理解为指针,在一般情况下,this 指向的是调用该函数的对象,而使用 函数 apply / call 方法可以改变 this 的指向(再次强调,因为函数也是对象,所以也会有属性和方法)。
function func(a,b,c) {
console.log(a,b,c)
console.log(this)
}
func.call({a:1},1,2,3) // 1,2,3 {a:1}
func.call('123',1,2,3) // String{"123"} {a:1}
func.apply({a:1},[1,2,3]) // 1,2,3 {a:1}
apply 和 call 的区别在于 apply 的第一个参数为即将执行的函数的 this 值,第二个参数为数组或者类数组,代表即将执行的函数的参数,而 call 第一个参数相同,第二个至最后的参数代表即将执行的参数
即 apply 会用数组保存函数的参数,call 则会平铺,除此以外没有区别(虽然 call 方法必须要将函数参数平铺,但是可以使用 ES6 的扩展运算符将其写为数组的形式,现在更推荐使用 call )。
另外 apply 和 call 都会让第一个参数进行装箱操作,即如果传入一个基本类型且基本类型有包装类型(下文会详细解释包装类型),则 this 的值为传入的基本类型的包装类型,例子中将 string 类型变成了 String 的包装类型。
在非严格模式下,如果 this 的值为 null / undefined,则自动会指向全局的 window 对象,而严格模式则不会有这个行为(值得注意的是这个并非 apply / call 的行为)。
function func() {
console.log(this)
}
func.call(undefined) // window 对象
function func2() {
"use strict"
console.log(this)
}
func2.call(undefined) // undefined
bind 和 apply / call 方法类似,也是一个用来改变函数 this 指向的方法,区别在于 bind 会返回一个被绑定 this 指向函数,而 apply / call 则直接会运行它,如果需要绑定 this 指向,又不想立即执行的话,可以使用 bind 方法,等需要使用时再调用绑定后的函数。
bind 第一个参数为 this 指向,第二个至以后的参数为给绑定的函数预先传入的参数,预置参数的函数通常也被称为偏函数。
function func(a,b,c,d) {
console.log(this)
console.log(a,b,c,d)
}
let boundFunc = func.bind({a:1},1,2)
boundFunc(3,4) // {a:1} 1,2,3,4
通过 bind 预先传入了参数 1,2,当调用绑定后的函数时,它会预先传入 1,2 作为第一第二个参数,此时再给函数传入参数,会作为第三第四的参数,最终打印 1,2,3,4。
为了便于操作基本类型的值,ECMAScript 提供了 3 个特殊的引用类型:
它们有别于 boolean,number,string ,可以发现它们首字母是大写,意味着它们是引用类型,也就是对象。
JS 高级程序设计中说到:
每当读取一个基本类型值的时候,后台就会创建一个对应的基本包装类型对象,从而让我们能够调用一些方法来操作这些数据.
var s1 = "some text"
var s2 = s1.substring(2)
我们要知道,基本类型是没有任何方法的,也就是说 "some text" 这个字符串是没有 substring 这个方法的,那为什么第二行代码不会报错呢?
原因在于,当第二行代码访问保存着字符串的变量 s1 时,会处于一种读取模式,尝试从内存中读取这个字符串的值,而在读取模式中访问字符串时,会有以下操作:
var s1 = "some text"
// 读取模式
s1 = new String("some text")
var s2 = s1.substring(2)
s1 = "some text" // 还原成基本类型
String 包装类型作为引用类型,它是含有 substring 方法的,所以需要将基本类型转换为包装类型才能执行方法,并将返回的结果赋值给变量 s2,最后再将 s1 还原成一开始的基本类型。
另外自动创建 (并非主动调用 new 创建的对象) 的包装类型只会存在于代码执行瞬间,然后立即销毁。
直接调用 String 函数生成的是基本类型,而使用 new 关键字将 String 作为构造函数使用,则生成的是包装类型。
let str = String("abc") // "abc" string 基本类型
let wrappedStr = new String("abc") // String {"abc"} String 包装类型
let boolean = Boolean(true) // true boolean 基本类型
let wrappedBoolean = new Boolean(true) // Boolean {true} Boolean 包装类型
let number = Number(123) // 123 number 基本类型
let wrappedNumber = new Number(123) // Number {123} Number 包装类型
一般情况下不推荐主动生成包装类型,因为容易和基本类型搞混。
toFixed 是 Number 包装类型下的一个方法,用于按照指定小数位返回数值的字符串表示,当数值小于传入当参数,会四舍五入。
let num = 10
console.log(num.toFixed(2)) // "10.00"
let num2 = 1.68
console.log(num.toFixed(1)) // "1.7"
但是使用 toFixed 时需要考虑到 Javascript 的小数的精度问题。
let num = 1.335
console.log(num.toFixed(2)) // "1.33"
console.log(num.toFixed(50)) // "1.3349999999999999644..."
事实上数字 1.335 在语言底层并不是真的以 1.335 来存储的,通过 toFixed 方法返回小数点后 50 位可以发现,1.335 真正的值为 1.3349999... 之后就是无限的循环,由于 JS 最多能表示的精度的长度是 16,所有的小数都只会精确到小数点后 16 位同时自动凑整,所以就进位之后就得到了 1.335由于代表的真实数字是 1.3349999...,所以 toFixed 四舍五入后的结果也就是 1.33 了,因为下一位是 4 被舍去了。
slice 方法同时可以使用于数组类型和 String 包装类型,具体的特点可以看上面的 Array 章节。
indexOf 方法同时可以使用于数组类型和 String 包装类型,它会从第一个位置开始遍历,寻找参数在字符串(数组)中的位置并返回下标,同时它还接受第二个参数用于在指定的位置之后开始寻找。
let str = "hello world"
// 从下标 6 的位置开始寻找字符串 o
// 但返回值仍相对于整个字符串的位置
console.log(str.indexOf('o',6)) // 7
尽管 ES6 有 includes 和 find 之类更强大的方法去寻找元素,但是如果需要从某个指定位置开始寻找元素,使用 indexOf 会更加的方便,以下例子会返回字符串中单词 e 的所有下标。
let str = `
React makes it painless to create interactive UIs.
Design simple views for each state in your application, and React will efficiently update and render just the right components when your data changes
`
let arr = []
let pos = str.indexOf('e')
while (pos > -1) {
arr.push(pos)
pos = str.indexOf('e',pos + 1)
}
console.log(arr) // [6,14,25,34,37,42,49,62,73,77,85,94,122,132,138,149,156,159,169,183,190,208]
trim 方法可以去除字符串首尾的空格,对于一些表单的输入是不允许有空格的,可以使用 trim 来去除,同时在 ES10 中,还有 trimStart 和 trimEnd 这两种方法,分别去除字符串前面和后面的空格。
replace 方法可以用来根据参数替换字符串,第一个参数可以是字符串也可以是正则,第二个参数可以是字符串也可以是函数。
str.replace(regexp|substr, newSubStr|function)
当参数都是字符串时,只是简单的在 str 中找到第一个参数第一次出现的位置,并替换成第二个参数。
当第一个参数是正则时,会替换和正则匹配的字符串,同时如果正则中含有 g 标志,会进行全局搜索,当第一次匹配到对应字符串后,不会停止匹配,而是继续往后搜索是否仍有可替换的字符串。
同时第二个参数还可以传入函数,匹配到的字符串会替换为函数的返回值,函数的引入使得 replace 方法更加的灵活。
let str = "hello world"
console.log(str.replace('l','x')) // "hexlo world" 只将第一个 l 替换为 x
console.log(str.replace(/l/,'x')) // "hexlo world" 同上
console.log(str.replace(/l/g,'x')) // "hexxo worxd" 通过给正则添加全局搜索的标志符,可以实现全局替换
console.log(str.replace(/l/, (match,index,str) => {
console.log(match) // 'l' 匹配的子串
console.log(index) // 2 匹配到的子字符串在原字符串中的偏移量
console.log(str) // 'hello world' 被匹配的原字符串
return 'q' // 返回字符串 q 代表将第一个匹配到的字符串 l 替换为 q
})) // "heqlo world"
关于 split 方法用来分割字符串,除了我们常用的传入一个字符串外,其实 split 还支持传入一个正则作为参数,如果是一个包含捕获组的正则表达式,会将捕获组也放入最终返回的数组中.
let str = "hello world"
console.log(str.split(/(l)/)) // [ 'he', 'l', '', 'l', 'o wor', 'l', 'd' ]
事实上,并没有所谓的全局变量或全局函数,所有在全局作用域中定义的属性和函数,都是 Global 对象的属性,包括原生的引用类型都是 Global 对象的属性, Global 对象一般不允许被直接访问。
之所以是 Global 对象而不是 window 对象,是因为 window 对象只在浏览器中存在,在 node 环境下,全局对象为 global,而在 webworker 中全局对象为 self,它们虽然都不是 Global 对象,但是都作为承担它的对象而存在。
还有一个内置对象是Math,它有很多很实用的方法,比如:
其中, min() 和 max() 方法用于确定一组数值中的最小值和最大值,ceil() 执行向上舍入,即它总是将数值向上舍入为最接近的整数,floor() 执行向下舍入,即它总是将数值向下舍入为最接近的整数,round() 执行标准舍入,即它总是将数值四舍五入为最接近的整数,random() 方法返回大于等于 0 小于 1 的一个随机数。
要找到数组中的最大或最小值,可以像下面这样使用 apply() 方法。
var values = [1, 2, 3, 4, 5, 6, 7, 8];
var max = Math.max.apply(Math, values);
上篇文章我们介绍了原型链和原型的概念,即每个对象都拥有一个原型对象,它们通过__proto__相互连接,而原型对象也会通过__proto__指向它的原型对象,这样一层层的指向,最终指向null,对象的从原型上继承方法和属性,这种关系被称为原型链。
上面这张图及清楚的表明了构造函数,原型,原型链之间的关系。
在Javascript中继承主要是通过原型链来实现的,我们知道,所有引用类型默认都继承了 Object ,而这个继承也是通过原型链实现的。大家要记住,所有函数的默认原型都是 Object 的实例,因此默认原型都会包含一个内部指针,指向 Object.prototype 。这也正是所有自定义类型都会继承 toString() 、valueOf() 等默认方法的根本原因。
继承意味着复制操作,但JavaScript(默认)并不会复制对象属性。相反,JavaScript 会在两个对象之间创建一个关联,这样一个对象就可以通过委托访问另一个对象的属性和函数。委托这个术语可以更加准确地描述 JavaScript 中对象的关联机制。
例如下面的这个例子:
function Foo(name) {
this.name = name;
}
Foo.prototype.getName = function() {
return this.name;
};
var a = new Foo( "a" );
a.getName(); // "a"
在这个例子中,a对象上并没有getName函数,但是a.getName并不会报错,并能打印出正确的值,我们之前已经介绍过原型链,当属性不存在对象上时,它会在原型链上一层层寻找,指导找到这个方法为止。实际上,我们在使用new创建新对象时,对象内部的[[Prototype]]都会关联到构造函数上的的.prototype上,我们看下面这张图:
我们可以看到a对象上的__proto__是指向了Foo构造函数的原型,原型上的constructor是指向Foo构造函数。
实例上当我们调用a.getName方法时发生了什么?
首先检查 a 对象是否具有可用的 getName() 方法。
如果没有,则检查 a 对象的原型对象(即 Foo.prototype)是否具有可用的 getName() 方法。
如果没有,则检查 Foo.prototype 所指向的对象的原型对象(即 Object.prototype)是否具有可用的 getName() 方法。这里有这个方法,于是该方法被调用。
原型链继承:
从上面我们可以得知,Javascript中的继承主要是靠原型链实现,实际上就是将对象的原型对象委托到一个新构造函数的实例上,通过原型链原理,就可以实现继承。看下面的代码:我们将Cat的原型委托到了一个Animal的实例上,这样Cat.prototype.proto 就会指向Animal.prototype,所以Cat实例上的__proto__也最后都会指向Animal.prototype,至此,我们就完整的实现了原型链继承。
function Animal() {
this.value = 'animal';
}
Animal.prototype.run = function() {
return this.value + ' is runing';
}
function Cat() {}
// 这里是关键,创建 Animal 的实例,并将该实例赋值给 Cat.prototype
// 相当于 Cat.prototype.__proto__ = Animal.prototype
Cat.prototype = new Animal();
var instance = new Cat();
instance.value = 'cat'; // 创建 instance 的自身属性 value
console.log(instance.run()); // cat is runing
原型链继承的缺点:
1、多个实例对引用类型的操作会被篡改
2、子类型的原型上的 constructor 属性被重写了
3、给子类型原型添加属性和方法必须在替换原型之后
4、创建子类型实例时无法向父类型的构造函数传参
#问题1
我们通过原型链来实现继承时,是用另一个构造函数的实例来替换掉原型对象的,而原型属性上的引用类型值会被所有实例共享,所以多个实例对引用类型的操作会被篡改。
如下面的代码:
function Animal() {
this.animal = ['cat', 'dog', 'monkey'];
}
function Cat() {}
Cat.prototype = new Animal();
var instance1 = new Cat();
instance1.animal.push('pig')
console.log(instance1.animal) // ['cat', 'dog', 'monkey', 'pig']
var instance2 = new Cat()
console.log(instance2.animal) // ['cat', 'dog', 'monkey', 'pig']
我们看到在instance1对引用属性进行操作,修改后的结果同样会在其他实例上体现出来。
#问题2
子类型原型上的constructor丢失了,在执行Cat.prototype = new Animal()后,Cat.prototype指向了Animal.prototype,所以Cat.prototype.constructor 指向了Animal。解决办法是,手动修改Cat.prototype.constructor,令Cat.prototype.constructor = Cat。
#问题3
给子类型原型添加属性和方法必须在替换原型之后,原因在第二点已经解释过了,因为子类型的原型会被覆盖。
function Animal() {
this.value = 'animal';
}
Animal.prototype.run = function() {
return this.value + ' is runing';
}
function Cat() {}
Cat.prototype = new Animal();
Cat.prototype.constructor = Cat;
// 新增
Cat.prototype.getValue = function() {
return this.value;
}
var instance = new Cat();
instance.value = 'cat';
console.log(instance.getValue()); // cat
如何解决原型链继承的问题,下期会专门讲JS中的其他继承方案。
《JavaScript 高级程序设计第三版》
Undefined 、 Null 、 Boolean 、 Number 和 String 这 5 种基本数据类型是按值访问的,因为可以操作保存在变量中的实际的值。
JavaScript 不允许直接访问内存中的位置,也就是说不能直接操作对象的内存空间。在操作对象时,实际上是在操作对象的引用而不是实际的对象。为此,引用类型的值是按引用访问的。
这里分两种情况:
let o1 = {}
let o2 = o1
o1.a = 'abc'
console.log(o1) // { a: 'abc' }
console.log(o2) // { a: 'abc' }
这里的o2实际上创建了一个新指针,它与o1一样,都指向了同一个对象,所以修改了o1后,同样也会在o2上表现出来。
但如果是基本类型的值,复制操作则会直接创建一个副本,意味着两者不会相互影响。
了解了这个知识点就可以知道为什么 JavaScript 会有深拷贝和浅拷贝这 2 个概念了,两者都是作用于引用类型,深拷贝和浅拷贝区别在于我们是想复制一个对象的引用还是想完完整整复制出一个相同的不会互相干扰的值出来。
这两个概念我会放到后面专门写一篇文章来讲解,这里不再赘述。
扩展运算符和 JSON.stringify 是比较常见的拷贝函数,前者用于浅拷贝,后者用于深拷贝。另外还有Object.assign函数也可以用作浅拷贝。
let arr = [1,2,3]
let shallowArr = [...arr]
let deepArr = JSON.parse(JSON.stringify(arr))
typeof 操作符可以很简单的确定变量是否是基本类型,如果变量的值是一个对象或 null ,则 typeof 操作符会像下面例子中所示的那样返回 "object"
let s = "Nicholas";
let b = true;
let i = 22;
let u;
let n = null;
let o = new Object();
console.log(typeof s); //string
console.log(typeof i); //number
console.log(typeof b); //boolean
console.log(typeof u); //undefined
console.log(typeof n); //object
console.log(typeof o); //object
对于进一步知道变量是哪种引用类型,需要使用 instanceof 操作符
let arr = []
console.log(typeof arr) // 'object'
console.log(arr instanceof Array) // true 表示它是一个数组的引用类型
instanceof操作符也有一个弊端,在它检测一个变量时,你必须提前知道它是哪一种Object的子类型,否则的话,也无法判断出变量属于哪一种引用类型。
let arr = []
console.log(arr instanceof Object) // true
这里返回 true,这样就无法判断 arr 变量是数组类型还是对象类型,导致这样的原因是数组类型是继承自对象类型,所以 instanceof 无法判断变量是由子类实例化还是由父类实例化的,所以又有了第三种解决方案Object.prototype.toString,它可以解决 instanceof 无法判断子类和父类的问题。
let arr = []
console.log(Object.prototype.toString.call(arr)) // "[object Array]"
然而理想总是美好的,现实总是骨感的,虽然 Object.prototype.toString 解决了具体是那种引用类型的问题,但是又引入了另外一个问题
它无法判断是基本类型还是基本包装类型
let str = 'abc'
let objStr = new String("abc")
console.log(Object.prototype.toString.call(str)) // "[object String]"
console.log(Object.prototype.toString.call(objStr)) // "[object String]"
可以看到,基本类型和基本包装类型始终都返回 "[object String]" ,这样仍无法区分具体的类型,但是我们可以将它和 typeof 结合。
const isType = variable => {
let type = typeof variable
if(type === 'object' || type === 'function'){
return Object.prototype.toString.call(variable).match( /\[object (\w+)]/)[1]
}else{
return type
}
}
console.log(isType('123')) // 'string'
console.log(isType(new String('123'))) // 'String'
执行环境也叫执行上下文,是当前 JavaScript 代码被解析和执行时所在环境的抽象概念。
每个函数都有自己的执行环境。当执行流进入一个函数时,函数的环境就会被推入一个环境栈中。而在函数执行之后,栈将其环境弹出,把控制权返回给之前的执行环境。当代码在一个环境中执行时,会创建变量对象的一个作用域链(scope chain)。
标识符解析是沿着作用域链一级一级地搜索标识符的过程。搜索过程始终从作用域链的前端开始,然后逐级地向后回溯,直至找到标识符为止(如果找不到标识符,通常会导致错误发生)。
由于在 innerFunc 中并没有变量 a,JS 引擎就会沿着作用域链,找到保存在 func 中的变量 a (如果 func 中仍没有,则会去全局作用域中寻找,再没有则返回 undefined)
目前的 JavaScript 有 3 种环境
有 3 种作用域
JavaScript 没有块级作用域经常会导致理解上的困惑。在其他类 C 的语言中,由花括号封闭的代码块都有自己的作用域(如果用 ECMAScript 的话来讲,就是它们自己的执行环境),因而支持根据条件来定义变量。
比如下面这段代码:
if (true) {
var color = "blue";
}
console.log(color); //"blue"
使用 var 声明的变量会自动被添加到最接近的环境中。在函数内部,最接近的环境就是函数的局部环境;如果初始化变量时没有使用 var 声明,该变量会自动被添加到全局环境。
而 ES6 后,使用 let/const 可以将其包裹的花括号成为块级作用域,并且使用 let/const 声明的变量不会有变量提升。
if(false) {
let color = "blue"
var otherColor = "red"
}
console.log('color' in window) // false
console.log('otherColor' in window) // true
Q: Chrome中如何找到需要设置断点的JS文件
A: ctrl + o
Q: Chrome中如何查看特定标签页占用的内存、CPU、网络使用情况
A: shift + esc
Q: 几个img标签放在一起的时候,有些浏览器会有默认的间距
A: CCS里: *{margin:0; padding:0}
或者使用float属性为img布局
Q: Chrome的调试器中Network标签下面,选中一个请求后,右侧Timing标签有什么用途
A: Timing标签中可以显示资源在整个请求生命周期过程中各部分时间花费信息
Q: Timing标签里面每个进度条分别代表什么含义
A:
Resource Scheduling(资源调度)
Queueing(排队)
排队的时间花费。可能由于该请求被渲染引擎认为是优先级比较低的资源(图片)、服务器不可用、超过浏览器的并发请求的最大连接数(Chrome的最大并发连接数为6)
Connection Start
Stalled
从HTTP连接建立到请求能够被发出送出去(真正传输数据)之间的时间花费。包含用于处理代理的时间,如果有已经建立好的连接,这个时间还包括等待已建立连接被复用的时间
Proxy Negotiation
与代理服务器连接的时间花费
DNS Lookup
执行DNS查询的时间。网页上每一个新的域名都要经过一个DNS查询。第二次访问浏览器有缓存的话,则这个时间为0
Initial connection
建立连接的时间花费,包含了TCP握手及重试时间
SSL
完成SSL握手的时间花费
Request/Response
Request sent
发起请求的时间
Waiting(Time to first byte (TTFB))
是最初的网络请求被发起到从服务器接收到第一个字节这段时间,它包含了TCP连接时间,发送HTTP请求时间和获得响应消息第一个字节的时间
Content Download
获取Response响应数据的时间花费
Q: src和href的区别
A:
src指向外部资源的位置, 用于替换当前元素, 比如js脚本, 图片等元素
href指向网络资源所在的位置, 用于在当前文档和引用资源间确定联系, 加载css
Q: 如果网页内容需要多语言,要怎么做
A:
采用统一编码utf-8模式
charset=uft-8 unicode多语言
Q: 请描述一下 cookies,sessionStorage 和 localStorage 的区别
A:
Cookie
cookie是网站为了标示用户身份而储存在用户本地终端(Client Side)上的数据(通常经过加密)
cookie数据始终在同源的http请求中携带(即使不需要),只会在浏览器和服务器间来回传递
cookie数据大小不能超过4k
设置的cookie过期时间之前一直有效,即使窗口或浏览器关闭
在所有同源窗口中都是共享的
sessionStorage
不会自动把数据发给服务器,仅在本地保存
存储大小可以达到5M或更大
数据在当前浏览器窗口关闭后自动删除
不在不同的浏览器窗口**享,即使是同一个页面
window.sessionStorage
localStorage
不会自动把数据发给服务器,仅在本地保存
存储大小可以达到5M或更大
存储持久数据,浏览器关闭后数据不丢失除非主动删除数据
在所有同源窗口中都是共享的
window.localStorage
Q: img设置属性title和alt的区别?
A:
Alt是img的特有属性, 或与<input type="image">
配合使用,规定图像的替代文本. 如果无法显示图像, 浏览器将显示替代文本. 用于图片无法加载显示、读屏器阅读图片,可提高图片可 访问性,搜索引擎会重点分析。最长可包含1024个字符
Title为元素提供附加的提示信息,用于鼠标滑到元素上的时候显示。其值可以比alt属性值设置的更长, 但是有些浏览器会截断过长的文字
Q:display:none 和 visibility: hidden的区别
A:
display:none
隐藏对应的元素,在文档布局中不再给它分配空间,它各边的元素会合拢,就当他从来不存在
visibility:hidden
隐藏对应的元素,但是在文档布局中仍保留原来的空间
Q: 什么是闭包
A: 闭包是指有权访问另一个函数作用域中的变量的函数. 创建闭包常见方式,就是在一个函数内部创建另一个函数
Q: 闭包有什么作用
A:
匿名自执行函数 (function (){ ... })(); 创建了一个匿名的函数,并立即执行它,由于外部无法引用它内部的变量,因此在执行完后很快就会被释放,关键是这种机制不会污染全局对象。
缓存, 可保留函数内部的值
实现封装
实现模板
Q: ajax请求是对createXMLHTTPRequest的封装,他有2个状态readyState和status用于判断返回状态,请说明下这2个状态有什么作用
A:
readyState是请求的状态
0 (未初始化): (XMLHttpRequest)对象已经创建,但还没有调用open()方法;
1 (载入):已经调用open() 方法,但尚未发送请求;
2 (载入完成): 请求已经发送完成;
3 (交互):可以接收到部分响应数据;
4 (完成):已经接收到了全部数据,并且连接已经关闭。
status是服务器方的返回状态
100——客户必须继续发出请求
101——客户要求服务器根据请求转换HTTP协议版本
200——成功
201——提示知道新文件的URL
300——请求的资源可在多处得到
301——删除请求数据
404——没有发现文件、查询或URl
500——服务器产生内部错误
Q: 关于js的垃圾回收
A:
离开作用域的值将被自动标记为可以回收, 因此将在垃圾收集期间被删除
"标记清除"是目前主流的垃圾收集算法, 这种算法的思路是给当前不使用的值加上标记, 然后再回收其内存。另一种垃圾收集算法是"引用计数", 这种算法的**是跟踪记录所有值被引用的次数. js引擎目前都不再使用这种算法, 但在IE中访问非原生JS对象(如DOM元素)时, 这种算法仍然可能会导致问题
当代码中存在循环引用现象时, "引用计数" 算法就会导致问题
解除变量的引用不仅有助于消除循环引用现象, 而且对垃圾收集也有好处. 为了确保有效地回收内存, 应该及时解除不再使用的全局对象, 全局对象属性以及循环引用变量的引用
Q: MVVM
A:
MVVM 是 Model-View-ViewModel 的缩写。
Model代表数据模型,也可以在Model中定义数据修改和操作的业务逻辑。
View 代表UI 组件,它负责将数据模型转化成UI 展现出来。
ViewModel 监听模型数据的改变和控制视图行为、处理用户交互,简单理解就是一个同步View 和 Model的对象,连接Model和View。在MVVM架构下,View 和 Model 之间并没有直接的联系,而是通过ViewModel进行交互,Model 和 ViewModel 之间的交互是双向的, 因此View 数据的变化会同步到Model中,而Model 数据的变化也会立即反应到View 上。
ViewModel 通过双向数据绑定把 View 层和 Model 层连接了起来,而View 和 Model 之间的同步工作完全是自动的,无需人为干涉,因此开发者只需关注业务逻辑,不需要手动操作DOM, 不需要关注数据状态的同步问题,复杂的数据状态维护完全由 MVVM 来统一管理。
Q: Vue的生命周期
A:Vue 实例从创建到销毁的过程,就是生命周期。从开始创建、初始化数据、编译模板、挂载Dom→渲染、更新→渲染、销毁等一系列过程,称之为 Vue 的生命周期。
Q: 生命周期阶段
A:
beforeCreate(创建前) 在数据观测和初始化事件还未开始
created(创建后) 完成数据观测,属性和方法的运算,初始化事件,$el属性还没有显示出来
beforeMount(载入前) 在挂载开始之前被调用,相关的render函数首次被调用。实例已完成以下的配置:编译模板,把data里面的数据和模板生成html。注意此时还没有挂载html到页面上。
mounted(载入后) 在el 被新创建的 vm.$el 替换,并挂载到实例上去之后调用。实例已完成以下的配置:用上面编译好的html内容替换el属性指向的DOM对象。完成模板中的html渲染到html页面中。此过程中进行ajax交互。
beforeUpdate(更新前) 在数据更新之前调用,发生在虚拟DOM重新渲染和打补丁之前。可以在该钩子中进一步地更改状态,不会触发附加的重渲染过程。
updated(更新后) 在由于数据更改导致的虚拟DOM重新渲染和打补丁之后调用。调用时,组件DOM已经更新,所以可以执行依赖于DOM的操作。然而在大多数情况下,应该避免在此期间更改状态,因为这可能会导致更新无限循环。该钩子在服务器端渲染期间不被调用。
beforeDestroy(销毁前) 在实例销毁之前调用。实例仍然完全可用。
destroyed(销毁后) 在实例销毁之后调用。调用后,所有的事件监听器会被移除,所有的子实例也会被销毁。该钩子在服务器端渲染期间不被调用。
Q: Vue的路由有哪些方式
A:
hash模式:在浏览器中符号“#”,#以及#后面的字符称之为hash,用window.location.hash读取;hash虽然在URL中,但不被包括在HTTP请求中;用来指导浏览器动作,对服务端安全无用,hash不会重加载页面。hash 模式下,仅 hash 符号之前的内容会被包含在请求中,如 http://www.xxx.com
,因此对于后端来说,即使没有做到对路由的全覆盖,也不会返回 404 错误。
history模式:history采用HTML5的新特性;且提供了两个新方法:pushState(),replaceState()可以对浏览器历史记录栈进行修改,以及popState事件的监听到状态变更。history 模式下,前端的 URL 必须和实际向后端发起请求的 URL 一致,如 http://www.xxx.com/items/id
。后端如果缺少对 /items/id 的路由处理,将返回 404 错误。Vue-Router 官网里如此描述:“不过这种模式要玩好,还需要后台配置支持……所以呢,你要在服务端增加一个覆盖所有情况的候选资源:如果 URL 匹配不到任何静态资源,则应该返回同一个 index.html 页面,这个页面就是你 app 依赖的页面。
Q: v-on 可以绑定多个方法吗
A: 可以
Q: 简述一下Sass、Less
A: 他们是动态的样式语言,是CSS预处理器,CSS上的一种抽象层。他们是一种特殊的语法/语言而编译成CSS。
Q: 说明Sass、Less区别
A:
变量符不一样,less是@,而Sass是$;
Sass支持条件语句,可以使用if{}else{},for{}循环等等。而Less不支持;
Sass是基于Ruby的,是在服务端处理的,而Less是需要引入less.js来处理Less代码输出Css到浏览器
**Q: 前后端分离,解决跨域问题的方法
A:
CORS(Cross-Origin Resource Sharing)跨域资源共享
'Access-Control-Allow-Origin:*'//或指定域 //指定允许其他域名访问
'Access-Control-Allow-Methods:GET,POST' //响应类型
'Access-Control-Allow-Headers:x-requested-with,content-type' //响应头设置
callback({"name","trigkit4"});
通过script标签引入一个js文件,这个js文件载入成功后会执行我们在url参数中指定的函数,并且会把我们需要的json数据作为参数传入。
JSONP的优点是:它不像XMLHttpRequest对象实现的Ajax请求那样受到同源策略的限制;它的兼容性更好,在更加古老的浏览器中都可以运行,不需要XMLHttpRequest或ActiveX的支持;并且在请求完毕后可以通过调用callback的方式回传结果。
JSONP的缺点则是:它只支持GET请求而不支持POST等其它类型的HTTP请求;它只支持跨域HTTP请求这种情况,不能解决不同域的两个页面之间如何进行JavaScript调用的问题。
浏览器都有一个同源策略,其限制之一就是第一种方法中我们说的不能通过ajax的方法去请求不同源中的文档.它的第二个限制是浏览器中不同域的框架之间是不能进行js的交互操作的。
window对象有个name属性,该属性有个特征:即在一个窗口(window)的生命周期内,窗口载入的所有的页面都是共享一个window.name的,每个页面对window.name都有读写的权限,window.name是持久存在一个窗口载入过的所有页面中的,并不会因新页面的载入而进行重置。
window.postMessage(message,targetOrigin) 方法是html5新引进的特性,可以使用它来向其它的window对象发送消息,无论这个window对象是属于同源或不同源,目前IE8+、FireFox、Chrome、Opera等浏览器都已经支持window.postMessage方法。调用postMessage方法的window对象是指要接收消息的那一个window对象,该方法的第一个参数message为要发送的消息,类型只能为字符串;第二个参数targetOrigin用来限定接收消息的那个window对象所在的域,如果不想限定域,可以使用通配符 * 。
websockets是一种浏览器的API,它的目标是在一个单独的持久连接上提供全双工、双向通信。(同源策略对web sockets不适用)web sockets原理:在js创建了web socket之后,会有一个HTTP请求发送到浏览器以发起连接。取得服务器响应后,建立的连接会使用HTTP升级从HTTP协议交换为web sockt协议。只有在支持web socket协议的服务器上才能正常工作。
图像ping是与服务器进行简单、单向的跨域通信的一种方式,请求的数据是通过查询字符串的形式发送的,而相应可以是任意内容,但通常是像素图或204相应(No Content)。 图像ping有两个主要缺点:首先就是只能发送get请求,其次就是无法访问服务器的响应文本。**
Q: Git 分支 - 分支的新建与合并
A: 查看分支:git branch
创建分支:git branch
切换分支:git checkout
创建+切换分支:git checkout -b
合并某分支到当前分支:git merge
删除分支:git branch -d
一、为什么JavaScript是单线程?
JavaScript语言的一大特点就是单线程,也就是说,同一个时间只能做一件事。那么,为什么JavaScript不能有多个线程呢?这样能提高效率啊。
JavaScript的单线程,与它的用途有关。作为浏览器脚本语言,JavaScript的主要用途是与用户互动,以及操作DOM。这决定了它只能是单线程,否则会带来很复杂的同步问题。比如,假定JavaScript同时有两个线程,一个线程在某个DOM节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准?
所以,为了避免复杂性,从一诞生,JavaScript就是单线程,这已经成了这门语言的核心特征,将来也不会改变。
为了利用多核CPU的计算能力,HTML5提出Web Worker标准,允许JavaScript脚本创建多个线程,但是子线程完全受主线程控制,且不得操作DOM。所以,这个新标准并没有改变JavaScript单线程的本质。
【这段没啥大问题,谢谢阮老师】
二、任务队列
单线程就意味着,所有任务需要排队,前一个任务结束,才会执行后一个任务。如果前一个任务耗时很长,后一个任务就不得不一直等着。
如果排队是因为计算量大,CPU忙不过来,倒也算了,但是很多时候CPU是闲着的,因为IO设备(输入输出设备)很慢(比如Ajax操作从网络读取数据),不得不等着结果出来,再往下执行。
JavaScript语言的设计者意识到,这时CPU完全可以不管IO设备,挂起处于等待中的任务,先运行排在后面的任务。等到IO设备返回了结果,再回过头,把挂起的任务继续执行下去。
【这个跟Brendan Eich没半毛钱关系。进程在处理IO操作的时候,操作系统多半自动将CPU切给其他进程用了】
于是,JavaScript就有了两种执行方式:一种是CPU按顺序执行,前一个任务结束,再执行下一个任务,这叫做同步执行;另一种是CPU跳过等待时间长的任务,先处理后面的任务,这叫做异步执行。程序员自主选择,采用哪种执行方式。
【纯粹扯蛋。】
【给CPU啥指令它就执行啥,哪有什么CPU跳过等待时间长的任务。】
【归根结底,阮老师没有懂什么叫异步。】
具体来说,异步执行的运行机制如下。(同步执行也是如此,因为它可以被视为没有异步任务的异步执行。)
【上面这句话表现出不仅不懂什么是异步,更不懂什么是同步。】
(1)所有任务都在主线程上执行,形成一个执行栈(execution context stack)。
(2)主线程之外,还存在一个"任务队列"(task queue)。系统把异步任务放到"任务队列"之中,然后继续执行后续的任务。
(3)一旦"执行栈"中的所有任务执行完毕,系统就会读取"任务队列"。如果这个时候,异步任务已经结束了等待状态,就会从"任务队列"进入执行栈,恢复执行。
(4)主线程不断重复上面的第三步。
【上面这段初步地在说event loop。但是异步跟event loop其实没有关系。准确的讲,event loop是实现异步的一种机制】
【一般而言,操作分为:发出调用和得到结果两步。发出调用,立即得到结果是为同步。发出调用,但无法立即得到结果,需要额外的操作才能得到预期的结果是为异步。同步就是调用之后一直等待,直到返回结果。异步则是调用之后,不能直接拿到结果,通过一系列的手段才最终拿到结果(调用之后,拿到结果中间的时间可以介入其他任务)。】
【上面提到的一系列的手段其实就是实现异步的方法,其中就包括event loop。以及轮询、事件等。】
【所谓轮询:就是你在收银台付钱之后,坐到位置上不停的问服务员你的菜做好了没。】
【所谓(事件):就是你在收银台付钱之后,你不用不停的问,饭菜做好了服务员会自己告诉你。】
下图就是主线程和任务队列的示意图。
只要主线程空了,就会去读取"任务队列",这就是JavaScript的运行机制。这个过程会不断重复。
【JavaScript运行环境的运行机制,不是JavaScript的运行机制。】
三、事件和回调函数
"任务队列"实质上是一个事件的队列(也可以理解成消息的队列),IO设备完成一项任务,就在"任务队列"中添加一个事件,表示相关的异步任务可以进入"执行栈"了。主线程读取"任务队列",就是读取里面有哪些事件。
【任务队列既不是事件的队列,也不是消息的队列。】
【任务队列就是你在主线程上的一切调用。】
【所谓的事件驱动,就是将一切抽象为事件。IO操作完成是一个事件,用户点击一次鼠标是事件,Ajax完成了是一个事件,一个图片加载完成是一个事件】
【一个任务不一定产生事件,比如获取当前时间。】
【当产生事件后,这个事件会被放进队列中,等待被处理】
"任务队列"中的事件,除了IO设备的事件以外,还包括一些用户产生的事件(比如鼠标点击、页面滚动等等)。只要指定过回调函数,这些事件发生时就会进入"任务队列",等待主线程读取。
所谓"回调函数"(callback),就是那些会被主线程挂起来的代码。异步任务必须指定回调函数,当异步任务从"任务队列"回到执行栈,回调函数就会执行。
【他们压根就没有被执行过,何来挂起之说?】
【异步任务不一定要回调函数。】
【从来就没有什么执行栈。主线程永远在执行中。主线程会不断检查事件队列】
"任务队列"是一个先进先出的数据结构,排在前面的事件,优先返回主线程。主线程的读取过程基本上是自动的,只要执行栈一清空,"任务队列"上第一位的事件就自动返回主线程。但是,由于存在后文提到的"定时器"功能,主线程要检查一下执行时间,某些事件必须要在规定的时间返回主线程。
【先产生的事件,先被处理。永远在主线程上,没有返回主线程之说】
【某些事件也不是必须要在规定的时间执行,有时候没办法在规定的时间执行】
四、Event Loop
主线程从"任务队列"中读取事件,这个过程是循环不断的,所以整个的这种运行机制又称为Event Loop(事件循环)。
【事件驱动的的实现过程主要靠事件循环完成。进程启动后就进入主循环。主循环的过程就是不停的从事件队列里读取事件。如果事件有关联的handle(也就是注册的callback),就执行handle。一个事件并不一定有callback】
为了更好地理解Event Loop,请看下图(转引自Philip Roberts的演讲《Help, I'm stuck in an event-loop》)。
【所以上面的callback queue,其实是event queue】
上图中,主线程运行的时候,产生堆(heap)和栈(stack),栈中的代码调用各种外部API,它们在"任务队列"中加入各种事件(click,load,done)。只要栈中的代码执行完毕,主线程就会去读取"任务队列",依次执行那些事件所对应的回调函数。
执行栈中的代码,总是在读取"任务队列"之前执行。请看下面这个例子。
var req = new XMLHttpRequest();
req.open('GET', url);
req.onload = function (){};
req.onerror = function (){};
req.send();
上面代码中的req.send方法是Ajax操作向服务器发送数据,它是一个异步任务,意味着只有当前脚本的所有代码执行完,系统才会去读取"任务队列"。所以,它与下面的写法等价。
var req = new XMLHttpRequest();
req.open('GET', url);
req.send();
req.onload = function (){};
req.onerror = function (){};
【等价个屁。这个调用其实有个默认回调函数,Ajax结束后,执行回调函数,回调函数检查状态,决定调用onload还是onerror。所以只要在回调函数执行之前设置这两个属性就行】
也就是说,指定回调函数的部分(onload和onerror),在send()方法的前面或后面无关紧要,因为它们属于执行栈的一部分,系统总是执行完它们,才会去读取"任务队列”。
五、定时器
除了放置异步任务,"任务队列"还有一个作用,就是可以放置定时事件,即指定某些代码在多少时间之后执行。这叫做"定时器"(timer)功能,也就是定时执行的代码。
定时器功能主要由setTimeout()和setInterval()这两个函数来完成,它们的内部运行机制完全一样,区别在于前者指定的代码是一次性执行,后者则为反复执行。以下主要讨论setTimeout()。
setTimeout()接受两个参数,第一个是回调函数,第二个是推迟执行的毫秒数。
console.log(1);
setTimeout(function(){console.log(2);},1000);
console.log(3);
上面代码的执行结果是1,3,2,因为setTimeout()将第二行推迟到1000毫秒之后执行。
如果将setTimeout()的第二个参数设为0,就表示当前代码执行完(执行栈清空)以后,立即执行(0毫秒间隔)指定的回调函数。
setTimeout(function(){console.log(1);}, 0);
console.log(2);
上面代码的执行结果总是2,1,因为只有在执行完第二行以后,系统才会去执行"任务队列"中的回调函数。
HTML5标准规定了setTimeout()的第二个参数的最小值(最短间隔),不得低于4毫秒,如果低于这个值,就会自动增加。在此之前,老版本的浏览器都将最短间隔设为10毫秒。
另外,对于那些DOM的变动(尤其是涉及页面重新渲染的部分),通常不会立即执行,而是每16毫秒执行一次。这时使用requestAnimationFrame()的效果要好于setTimeout()。
需要注意的是,setTimeout()只是将事件插入了"任务队列",必须等到当前代码(执行栈)执行完,主线程才会去执行它指定的回调函数。要是当前代码耗时很长,有可能要等很久,所以并没有办法保证,回调函数一定会在setTimeout()指定的时间执行。
【定时器并不是特例。到达时间点后,会形成一个事件(timeout事件)。不同的是一般事件是靠底层系统或者线程池之类的产生事件,但定时器事件是靠事件循环不停检查系统时间来判定是否到达时间点来产生事件】
六、Node.js的Event Loop
Node.js也是单线程的Event Loop,但是它的运行机制不同于浏览器环境。
【以我对Node的了解,上面这个图也是错的。】
【OS Operation不在那个位置,而是在event loop的后面。event queue在event loop中间】
【js —> v8 —> node binding —> (event loop) —> worker threads/poll —> blocking operation> <— <— <—— (event loop)<—————— event <——————】
根据上图,Node.js的运行机制如下。
(1)V8引擎解析JavaScript脚本。
(2)解析后的代码,调用Node API。
(3)libuv库负责Node API的执行。它将不同的任务分配给不同的线程,形成一个Event Loop(事件循环),以异步的方式将任务的执行结果返回给V8引擎。
(4)V8引擎再将结果返回给用户。
【完全不是不同的任务分配给不同的线程。只有磁盘IO操作才用到了线程池(unix)。】
【Node中,磁盘I/O的异步操作步骤如下:】
【将调用封装成中间对象,交给event loop,然后直接返回】
【中间对象会被丢进线程池,等待执行】
【执行完成后,会将数据放进事件队列中,形成事件】
【循环执行,处理事件。拿到事件的关联函数(callback)和数据,将其执行】
【然后下一个事件,继续循环】
除了setTimeout和setInterval这两个方法,Node.js还提供了另外两个与"任务队列"有关的方法:process.nextTick和setImmediate。它们可以帮助我们加深对"任务队列"的理解。
process.nextTick方法可以在当前"执行栈"的尾部----主线程下一次读取"任务队列"之前----触发回调函数。也就是说,它指定的任务总是发生在所有异步任务之前。setImmediate方法则是在当前"任务队列"的尾部触发回调函数,也就是说,它指定的任务总是在主线程下一次读取"任务队列"时执行,这与setTimeout(fn, 0)很像。请看下面的例子(via StackOverflow)。
process.nextTick(function A() {
console.log(1);
process.nextTick(function B(){console.log(2);});
});
setTimeout(function timeout() {
console.log('TIMEOUT FIRED');
}, 0)
// 1
// 2
// TIMEOUT FIRED
上面代码中,由于process.nextTick方法指定的回调函数,总是在当前"执行栈"的尾部触发,所以不仅函数A比setTimeout指定的回调函数timeout先执行,而且函数B也比timeout先执行。这说明,如果有多个process.nextTick语句(不管它们是否嵌套),将全部在当前"执行栈"执行。
现在,再看setImmediate。
setImmediate(function A() {
console.log(1);
setImmediate(function B(){console.log(2);});
});
setTimeout(function timeout() {
console.log('TIMEOUT FIRED');
}, 0)
// 1
// TIMEOUT FIRED
// 2
上面代码中,有两个setImmediate。第一个setImmediate,指定在当前"任务队列"尾部(下一次"事件循环"时)触发回调函数A;然后,setTimeout也是指定在当前"任务队列"尾部触发回调函数timeout,所以输出结果中,TIMEOUT FIRED排在1的后面。至于2排在TIMEOUT FIRED的后面,是因为setImmediate的另一个重要特点:一次"事件循环"只能触发一个由setImmediate指定的回调函数。
我们由此得到了一个重要区别:多个process.nextTick语句总是一次执行完,多个setImmediate则需要多次才能执行完。事实上,这正是Node.js 10.0版添加setImmediate方法的原因,否则像下面这样的递归调用process.nextTick,将会没完没了,主线程根本不会去读取"事件队列”!
【10.0版就不用纠正了吧】
process.nextTick(function foo() {
process.nextTick(foo);
});
事实上,现在要是你写出递归的process.nextTick,Node.js会抛出一个警告,要求你改成setImmediate。另外,由于process.nextTick指定的回调函数是在本次"事件循环"触发,而setImmediate指定的是在下次"事件循环"触发,所以很显然,前者总是比后者发生得早,而且执行效率也高(因为不用检查"任务队列")。
关于setImmediate与setTimeout(fn,0)的区别是,setImmediate总是在setTimeout前面执行,除了主线程第一次进入Event Loop时。请看下面的例子。
setTimeout(function () {
console.log('1');
},0);
setImmediate(function () {
console.log('2');
})
上面代码的运行结果不确定,有可能是1,2,也有可能是2,1,即使setTimeout和setImmediate两个函数互换位置,也是如此。因为这些代码是主线程第一次读取Event Loop之前运行。但是,如果把这段代码放在setImmediate之中,结果就不一样。
setImmediate(function () {
setTimeout(function () {
console.log('1');
},0);
setImmediate(function () {
console.log('2');
})
})
上面代码运行结果总是2,1,因为进入Event Loop之后,setImmediate在setTimeout之前触发。
【还是会出现1, 2的情况。呵呵。不信试试】
(完)
【准确讲,使用事件驱动的系统中,必然有非常非常多的事件。如果事件都产生,都要主循环去处理,必然会导致主线程繁忙。那对于应用层的代码而言,肯定有很多不关心的事件(比如只关心点击事件,不关心定时器事件)。这会导致一定浪费。】
【这篇文章里没有讲到的一个重要概念是watcher。观察者。】
【事实上,不是所有的事件都放置在一个队列里。】
【不同的事件,放置在不同的队列。】
【当我们没有使用定时器时,则完全不用关心定时器事件这个队列】
【当我们进行定时器调用时,首先会设置一个定时器watcher。事件循环的过程中,会去调用该watcher,检查它的事件队列上是否产生事件(比对时间的方式)】
【当我们进行磁盘IO的时候,则首先设置一个io watcher,磁盘IO完成后,会在该io watcher的事件队列上添加一个事件。事件循环的过程中从该watcher上处理事件。处理完已有的事件后,处理下一个watcher】
【检查完所有watcher后,进入下一轮检查】
【对某类事件不关心时,则没有相关watcher】
【最后,如有问题,谢谢指出】
JS中有5种简单数据类型(也称基本数据类型)和一种复杂数据类型(也称引用类型)即Object,typeof操作符就是用用来检测给定变量的数据类型的。如果要区分引用类型可以用instanceof操作符。
注意:null值是基本类型,但是用typeof检测时会返回Object,这是因为null值表示一个空对象指针。
我们知道的对象是无序属性的集合,其属性可以包含基本、对象或者函数。创建对象的实例有两种方式,第一种是使用new 操作符后跟Object构造函数,例如:
var people = new Object();
peopel.name = 'Chaeles'
people.age = 10
第二种是使用对象字面量
var people = { name: 'Charles', age: 10 }
这两种方式都可以很方便的用来创建单个对象,但是也有一个很明显的缺点就是使用一个接口会创建很多重复代码。
工厂模式
用函数来封装以特定接口创建对象的细节,例如下面
function createPeople(name,age){ var o = new Object(); o.name = name; o.age = age; return o; }
var people1 = createObject('Charles',10')
构造函数虽然解决了多个相似对象的问题,但却没有解决对象识别的问题(即怎样知道一个对象的类型)于是构造函数模式出现了。
构造函数
function People (name,age) { this.name = name; this.age = age; }
var people1 = new People('Charles', 10)
构造函数模式与工厂模式相比,省去了在内部new Object即显式的创造对象,并且直接将属性和方法赋值给了this,并且省去了return语句。
所以要创建People的新shi实例,必须使用new 操作符。
但其实构造函数也有缺点,如果在构造函数内部定义一个方法,用这个构造函数定义的所有实例都会具有这个方法,这就造成巨大的内存浪费。于是原型模式出现了。
什么是原型模式,我们创建的每个函数有一个prototype属性,(注意:也包括构造函数)这个属性是一个指针,指向一个对象,而这个对象的用途是包含可以由特定类型的所有实例共享的属性和方法。也就是通过调用构造函数而创建的那个对象实例的原型对象。使用原型对象的好处是可以让所有对象实例共享它所包含的属性和方法。
代码如下:
function People(){};
People.prototype.name = 'Charles';
People.prototype.age = 26;
var people1 = new People()
people1.name = 'Charles'
var people2 = new People()
people2.name = 'Charles'
从这里可以看出,在prototype上定义的属性会被所有构造函数的实例所共享,这个prototype对象就是函数的原型对象。在默认情况下,所有的原型对象都会自动获得一个constructor(构造函数)属性,这个属性包含一个指向prototype属性所在函数的指针。
也就是说每当我们自定义一个构造函数之后,其原型对象上默认只会取得constructor属性,至于其他方法都是从Object上继承而来。如图:
我们可以看到People的构造函数上有一个prototype属性指向它的原型对象,并且它的原型对象上有一个constructor属性又指向了People构造函数。
当调用构造函数创建一个新实例后,该实例的内部将包含一个指针(注意:这是一个内部属性)指向构造函数的原型对象,这个属性就是[[Prototype]],但是在脚本中是没有标准的方法去访问这个属性,但是浏览器在每一个对象上都支持一个属性__proto__,也就是隐式对象指针。如图:
继承是OO语言中的一个概念,继承有两种方法,接口继承和实现继承。接口继承制继承方法签名,实现继承则继承实际的方法。在ECMAScript中,由于函数没有签名,没有办法实现接口继承。所以ECMAScript只支持实现继承,而实现继承主要是依靠原型链来实现的。也可以说成,原型链是javascript里用来解决继承的。
原型链的基本**是利用原型让一个引用类型继承另一个引用类型的属性和方法。我们回顾一下构造函数、原型和实例的关系:每个构造函数都有一个原型对象,原型对象都包含一个指向构造函数的指针,而实例都包含一个指向原型对象的内部指针。那么,假入我们让原型对象等于另一个类型的实例,此时的原型对象将包含一个指向另一个原型的指针,相应地,另一个原型中也包含着一个指向另一个构造函数的指针。假如另一个原型有事另一个类型的实例,那么上述关系依然成立,如此层层递进,就构成了实例与原型的链条。这就是原型链的基本概念。大致代码如下:
function SuperType () { this.prototype = true; }
SuperType.prototype.getSuperValue = function () { return this.property; }
function SubType () { this.subproperty = false; }
SubType.prototype = new SuperType();
SubType.prototype.getSubValue = function () { return this.subproperty; }
var instanse = new SubType();
alert(instanse.getSuperValue()) // true
在上面的代码中,我们没有使用SubType默认提供的原型,而是给它换了一个新原型,这个新原型就是SuperType 的实例。于是,新原型不仅具有作为一个SuperType的实例拥有的全部属性和方法,而且其内部还有一个指针,指向了SuperType 的原型。
这样就实现了原型链。
我们看到所有函数都可以通过__proto__访问到Function,而Function.prototype可以通过__proto__访问到Object.prototype.
最后总结一下,所有的引用类型默认都继承了Object,而这个继承也是通过原型链实现的。所有函数的默认原型都是Object的实例,因此默认原型都会包含一个内部指针,指向Object.prototype。
在所有的编程语言中,最基本的功能就是能够储存变量当中的值,并且能在之后对这个值进行访问或修改。但是,将变量引入程序后有几个问题需要我们讨论一下,例如:这些变量住在哪里?换句话说,它们储存在哪里?最重要的是,程序需要时如何找到它们?
这些问题说明需要一套设计良好的规则来存储变量,并且之后可以方便地找到这些变量。这套规则被称为作用域。
JS中的作用域就是词法作用域,简单来说:词法作用域就是定义在词法阶段的作用域。换句话说,词法作用域是由你在写代码时将变量和块作用域写在哪里来决定的。
与词法作用域相对的就是动态作用域:
动态作用域并不关心函数和作用域是如何声明以及在何处声明的,只关心它们从何处调用。
来看下面的代码:
var a = 2;
function foo() {
console.log( a ); // 2
}
function bar() {
var a = 3;
foo();
}
bar();
假设JavaScript采用静态作用域,让我们分析下执行过程:
执行 foo 函数,先从 foo 函数内部查找是否有局部变量 a,如果没有,就根据书写的位置,查找上面一层的代码,也就是 a 等于 2,所以结果会打印 2。
假设JavaScript采用动态作用域,让我们分析下执行过程:
执行 foo 函数,依然是从 foo 函数内部查找是否有局部变量 value。如果没有,就从调用函数的作用域,也就是 bar 函数内部查找 a 变量,所以结果会打印 3。
前面我们已经说了,JavaScript采用的是静态作用域,所以这个例子的结果是 2。
在JS中有一个重要的概念,就是执行环境(execution context),它定义了变量或函数有权访问的其他数据,决定了它们各自的行为,每个执行环境都有一个与之关联的变量对象(variable object),环境中定义的所有变量和函数都保存在这个对象中。
当代码在一个环境中执行时,会创建变量对象的一个作用域链(scope chain),当访问一个变量时,解释器会首先在当前作用域查找标示符,如果没有找到,就去父作用域找,直到找到该变量的标示符或者不在父作用域中,这就是作用域链。
在 Web 浏览器中,全局执行环境被认为是 window 对象,因此所有全局变量和函数都是作为window 对象的属性和方法创建的。
// my_script.js
"use strict";
var foo = 1;
var bar = 2;
在全局环境中,创建了两个简单地变量。如前面所说,此时变量对象是全局对象:
执行上述代码,my_script.js本身会形成一个执行环境,以及它所引用的变量对象。
// my_script.js
"use strict";
var foo = 1;
var bar = 2;
function myFunc() {
var a = 1;
var b = 2;
var foo = 3;
console.log("inside myFunc");
}
console.log("outside");
myFunc();
定义时:当myFunc被定义的时候,myFunc的标识符(identifier)就被加到了当前的作用域对象中(在这里就是全局对象),并且这个标识符所引用的是一个函数对象(function object)。函数对象中所包含的是函数的源代码以及其他的属性。其中一个我们所关心的属性就是内部属性[[scope]]。[[scope]]所指向的就是当前的作用域对象。也就是指的就是函数的标识符被创建的时候,我们所能够直接访问的那个作用域对象(在这里就是全局对象)。
myFunc所引用的函数对象,其本身不仅仅含有函数的代码,并且还含有指向其被创建的时候的作用域对象。
调用时:当myFunc函数被调用的时候,一个新的作用域对象被创建了。新的作用域对象中包含myFunc函数所定义的本地变量,以及其参数(arguments)。这个新的作用域对象的父作用域对象就是在运行myFunc时我们所能直接访问的那个作用域对象。
如前面所说,当函数返回没有被引用的时候,就会被垃圾回收器回收。但是对于闭包(函数嵌套是形成闭包的一种简单方式)呢,即使外部函数返回了,函数对象仍会引用它被创建时的作用域对象。
"use strict";
function createCounter(initial) {
var counter = initial;
function increment(value) {
counter += value;
}
function get() {
return counter;
}
return {
increment: increment,
get: get
};
}
var myCounter = createCounter(100);
console.log(myCounter.get()); // 返回 100
myCounter.increment(5);
console.log(myCounter.get()); // 返回 105
当调用 createCounter(100) 时,对象之间的关系如下图所示:
内嵌函数increment和get都有指向createCounter(100) scope的引用。如果createCounter(100)没有任何返回值,那么createCounter(100) scope不再被引用,于是就可以被垃圾回收。但是因为createCounter(100)实际上是有返回值的,并且返回值被存储在了myCounter中,所以对象之间的引用关系变成了如下图所示:
需要用点时间思考的是:即使createCounter(100)已经返回,但是其作用域仍在,并能且只能被内联函数访问。可以通过调用myCounter.increment() 或 myCounter.get()来直接访问createCounter(100)的作用域。
当myCounter.increment() 或 myCounter.get()被调用时,新的作用域对象会被创建,并且该作用域对象的父作用域对象会是当前可以直接访问的作用域对象。此时,引用关系如下:
当执行到return counter;时,在get()所在的作用域并没有找到对应的标示符,就会沿着作用域链往上找,直到找到变量counter,然后返回该变量。
调用increment(5)则会更有意思:
当单独调用increment(5)时,参数value会存贮在当前的作用域对象。函数要访问value,能马上在当前作用域找到该变量。但是当函数要访问counter时,并没有找到,于是沿着作用域链向上查找,在createCounter(100)的作用域找到了对应的标示符,increment()就会修改counter的值。除此之外,没有其他方式来修改这个变量。闭包的强大也在于此,能够存贮私有数据。
创建两个函数:myCounter1和myCounter2
//my_script.js
"use strict";
function createCounter(initial) {
/* ... see the code from previous example ... */
}
//-- create counter objects
var myCounter1 = createCounter(100);
var myCounter2 = createCounter(200);
myCounter1.increment和myCounter2.increment的函数对象拥有着一样的代码以及一样的属性值(name,length等等),但是它们的[[scope]]指向的是不一样的作用域对象。
本文是我重新看 《Javascript高级程序设计》 的读书笔记的第一篇,也是万丈高楼系列(基础部分)的第一篇,将来会分几个部分来介绍和分析js的基本概念和注意事项。文中还配有相对应的题目来考察,本人水平有限,如有错误,望请指正,非常感谢。
ECMAScript中有5种简单数据类型(也称为基本数据类型):Undefined、Null、Boolean、Number和String。还有1种复杂数据类型——Object,Object本质上是由一组无序的名值对组成的。ECMAScript不支持任何创建自定义类型的机制,而所有值最终都将是上述6种数据类型之一。
上面这段话引用自《Javascript高级程序设计》第23页的数据类型的开端。
从这里我们可以看到ECMAScript(以下简称ES)中只有6种数据类型,但是此书介绍的是ES3中的定义,到现在为止已经发布到了ES2019,也就是ES10。在后来的版本中增加了很多数据类型。增加了简单类型如Symbol类型,复杂数据类型如Set类型,Map类型,WeakSet类型,WeakMap类型,TypedArray类型等。下面会一一介绍这些数据类型,并对书中内容做一个补充。
既然有类型,我们就要知道如何去判断一个数据它是什么类型的,就有了typeof操作符,它就是专门来提供这方面的信息的,
对一个值使用typeof操作符可能返回下列某个字符串:
但是typeof不能判断复杂数据类型,在判断复杂数据类型时,它会全部返回object,这个如何避免我们会在后面说道,现在我们只用它来判断普通数据类型。
从技术角度来讲,函数在ES中是一种对象,不是一种数据类型。
Undefined 类型只有一个值,即特殊的 undefined。在使用 var 声明变量但未对其加以初始化时,这个变量的值就是 undefined,请看下面代码:
var message;
alert(message == undefined); //true
在javascript中未经初始化的值默认就会取得undefined值。
那如果对尚未声明过的变量执行typeof检测会出现声明呢,请看以下代码:
var message; // 这个变量声明之后默认取得了 undefined 值
// 下面这个变量并没有声明
// var age
alert(typeof message); // "undefined"
alert(typeof age); // "undefined"
在js中对未声明的变量只能进行typeof类型检测操作。
结果表明,对未初始化和未声明的变量执行typeof操作符都返回了undefined值;这个结果有其逻辑上的合理性。因为虽然这两种变量从技术角度看有本质区别,但实际上无论对哪种变量也不可能执行真正的操作。
虽然未初始化的变量会被自动赋值为undefined值,但在实际开发中,我们应该尽量为每个变量都赋于一个初始值,这样当我们用typeof检测类型时返回的是undefined时,我们就知道变量是尚未声明,而不是未被初始化。
Null类型是第二个只有一个值的数据类型,这个特殊的值就为null。我们用 typeof 操作符对它进行类型判断时发现,它返回的是字符串"object",这说明它在javascript中表示的是一个对象。
请看以下代码:
var car = null
alert(typeof car) // object
这里书上的原话是 “ 从逻辑角度来看: null值表示的一个空对象指针,而这也是使用typeof操作符检测null值时会返回"object"的原因。 ” 说实话,这里的解释很迷,他只是说明null值表示一个空对象指针,并未说明它为何是一个空对象指针。在网上找这个问题时,很多都提到一个原因,就是js在开发时考虑不够,这是历史的一个遗留问题。
我的理解是:参考上面的undefined类型,在一个变量声明但未经初始化时会被默认赋予为undefined值,我们也可以显式的把一个变量初始化undefined值,虽然这样没有任何意义。是否可以这样理解,在我们想要把一个变量初始化为基本类型的值但最后并未进行初始化时,与我们想要把一个变量初始化为复杂类型的值一样,null值表示我们想要把一个变量初始化为复杂类型但没有进行初始化的一个占位符,简单点来说,代表对象值故意留空的一个原始值,代表此处期望是一个对象的值,这就是它代表一个空对象指针的真正原因。
MDN解释: 在 JavaScript 最初的实现中,JavaScript 中的值是由一个表示类型的标签和实际数据值表示的。对象的类型标签是 0。由于 null 代表的是空指针(大多数平台下值为 0x00),因此,null 的类型标签是 0,typeof null 也因此返回 "object"。
在实际开发中,只要意在保存对象的变量还没有真正保存对象,我们就应该明确的让该变量保存null值,这样做不仅可以体现null作为空对象指针的惯例,而且也有助于进一步区分null和undefined。
Boolean类型只有两个字面值:true和false。 注意这里的值是区分大小写的,只有小写状态的值才是Boolean值。其他类型的值都可以转换为Boolean值。
在javascript中Number类型是使用IEEE754格式来表示整数和浮点数值(也被称作双精度数值),javascript支持十进制、八进制、和十六进制的表示格式,但在严格模式下,八进制字面量是无效的。在进行算数计算时,所有以八进制和十六进制表示的数值都将最终被转换成十进制。
在javascript中0.1 + 0.2 是不等于 0.3 的(约等于 0.30000000000000004),这是使用基于IEEE754数值的浮点计算的通病。
javascript 中数值的最大值和最小值 是存储在 Number.MAX_VALUE (1.7976931348623157e+308)Number.MIN_VALUE (5e-324)中的,但是如果某次计算超出了这些范围,会被自动转换成特殊的Infinity,如果是负数,就是-Infinity,判断一个数是否是有穷的,可以用isFinite()函数。
Number.MAX_SAFE_INTEGER 和 Number.MIN_SAFE_INTEGER 是可以在计算中安全使用的最大整数和最小整数。 介于最大值和安全值之间的值都不能精确表示,这时才会下面的BigInt类型。
访问 Number.NEGATIVE_INFINITY 和 Number.POSITIVE_INFINITY 也可以得到负和正 Infinity 的值。可以想见,这两个属性中分别保存着 -Infinity 和 Infinity
NaN ,即非数值(Not a Number)是一个特殊的数值,这个数值用于表示一个本来要返回数值的操作数未返回数值的情况.检测一个变量是否为NaN可以用isNaN函数
有 3 个函数可以把非数值转换为数值: Number() 、 parseInt() 和 parseFloat() 。第一个函数,即转型函数 Number() 可以用于任何数据类型,而另两个函数则专门用于把字符串转换成数值。这3个函数对于同样的输入会有返回不同的结果。
Number() 函数的转换规则如下:
这里的类型转换是显式类型转换,除了Number()函数还有String()和Boolean()函数,它们会在编译阶段转换数据类型,与显式对应的有隐式类型转换,当使用 ==、&&、|| 等逻辑操作符进行判断时, 或使用 + - * / 四则运算符进行操作时会触发隐式类型转换。
parseInt()和parseFloat()函数都是从第一个字符(位置0)开始解析每个字符,它们都可以转换字符串为数字,它们在转换空字符串时与Number()函数有区别,空字符串它们会返回NaN,但Number会返回0。
parseInt()可以识别出各种整数格式,但是八进制只有在ES3 javascript引擎中才能正确识别,ES5 中已经不支持识别八进制了,为了修复这个问题,可以给这个函数加上第二个参数,第二个参数就是转换时的基数。
parseFloat() 的解析规则时一直解析直到遇到一个无效的浮点数为止,也就是说字符串中第一个小数点是有效的,而第二个就无效了,另外一个重要的区别就是它始终会忽略掉前导0,并且没有第二个参数选项。
要把一个值转换为一个字符串有两种方式。第一种是使用几乎每个值都有的 toString() 方法,数值、布尔值、对象和字符串值(没错,每个字符串也都有一个 toString() 方法,该方法返回字符串的一个副本)都有 toString() 方法。但 null 和 undefined 值没有这个方法。
在不知道要转换的值是不是 null 或 undefined 的情况下,还可以使用转型函数 String() ,这个函数能够将任何类型的值转换为字符串。 String() 函数遵循下列转换规则:
Object 的每个实例都具有下列属性和方法。
Symbol函数用的比较少,我们只要记住,每个从Symbol()返回的symbol值都是唯一的,一个symbol值能作为对象属性的标识符(这可能是它唯一的作用了),并且不支持 new Symbol()语法。
因为Number.MAX_SAFE_INTEGER是JS中所能精确表达的最大整数,它的值为2^53,介于MAX_VALUE和MAX_SAFE_INTEGER之间的值无法被精确表达,为了解决这一问题,在ES10中引入了一个全新的数字类型--BigInt类型(目前还处于stage3阶段),它就是为了解决安全值和最大值之间的值不能被精表达的问题。
它非常容易使用:
// 字面量只需要在整数后面添加n后缀
const bigInt = 1000n
// 也可以通过BigInt()的形式生成
const bigInt2 = BigInt(1001)
注意事项:不能用与 Math 对象中的方法;不能和任何 Number 实例混合运算,两者必须转换成同一种类型。在两种类型来回转换时要小心,因为 BigInt 变量在转换成 Number 变量时可能会丢失精度。
至此,目前js里的所有数据类型都总结完毕,谢谢阅读到这里,如有错误,欢迎指出。
闭包是指那些能够访问自由变量的函数。
其中自由变量,指在函数中使用的,但既不是函数参数arguments也不是函数的局部变量的变量,其实就是另外一个函数作用域中的变量。
举个例子:
var a = 1
function foo() {
console.log(a)
}
foo()
这里的a 既不是函数参数也不是foo的局部变量,那它就是自由变量。
那么,函数 foo + foo 函数访问的自由变量 a 不就是构成了一个闭包嘛……
确实如此,从技术角度上来讲,所有的Javascript 函数都是闭包。
还有个实践角度上的闭包,我们先来看看定义:
ECMAScript中,闭包指的是:
从理论角度:所有的函数。因为它们都在创建的时候就将上层上下文的数据保存起来了。哪怕是简单的全局变量也是如此,因为函数中访问全局变量就相当于是在访问自由变量,这个时候使用最外层的作用域。
从实践角度:以下函数才算是闭包:
i. 即使创建它的上下文已经销毁,它仍然存在(比如,内部函数从父函数中返回)
ii. 在代码中引用了自由变量
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 函数上下文已经被销毁了啊(即从执行上下文栈中被弹出),怎么还会读取到 checkscope 作用域下的 scope 值呢?
当我们了解了具体的执行过程后,我们知道 f 执行上下文维护了一个作用域链:
fContext = {
Scope: [AO, checkscopeContext.AO, globalContext.VO],
}
对的,就是因为这个作用域链,f 函数依然可以读取到 checkscopeContext.AO 的值,说明当 f 函数引用了 checkscopeContext.AO 中的值的时候,即使 checkscopeContext 被销毁了,但是 JavaScript 依然会让 checkscopeContext.AO 活在内存中,f 函数依然可以通过 f 函数的作用域链找到它,正是因为 JavaScript 做到了这一点,从而实现了闭包这个概念。
所以,让我们再看一遍实践角度上闭包的定义:
防抖(Debounce)和节流(Throttle)都是用来控制某个函数在一定时间内执行多少次的技巧,两者相似而又不同。
所谓防抖,就是指触发事件后,就是把触发非常频繁的事件合并成一次去执行。即在指定时间内只执行一次回调函数,如果在指定的时间内又触发了该事件,则回调函数的执行时间会基于此刻重新开始计算。
如图可以看到,一段时间内频繁的触发某一函数,只会被当成一次来执行,具体的实现方法是采用setTimeout和一个固定变量timeId,每次触发函数时都会检查timeId是否存在,存在即会销毁之前的定时器并重新定时,直到这是最后一次触发。
debounce(fn, delayTime) { var timeId; return function() { var context = this, args = arguments; timeId && clearTimeout(timeId) timeId = setTimeout(() => { fn.apply(context, args) }, delayTime); } },
思路解析:
执行debounce函数之后会返回一个新的函数,通过闭包的形式,维护一个变量timeId,每次执行该函数的时候会结束之前的延迟操作,重新执行setTimeout方法,也就实现了上面所说的指定的时间内多次触发同一个事件,会合并执行一次。
所谓节流,是指频繁触发事件时,只会在指定的时间段内执行事件回调,即触发事件间隔大于等于指定的时间才会执行回调函数。
跟 debounce 主要的不同在于,throttle 保证 X 毫秒内至少执行一次。
throttle(fn, delayTime) { var flag, _start = Date.now(); return function() { var context = this, args = arguments, _now = Date.now(), remainTime = delayTime - (_now - _start); if (remainTime <= 0) { fn.apply(this, args); } else { setTimeout(function() { fn.apply(this, args); }, remainTime); } }; }
频繁触发事件时,函数防抖只会在最后一次触发事件只会才会执行回调内容,其他情况下会重新计算延迟事件,而函数节流便会很有规律的每隔一定时间执行一次回调函数。
之前,我们使用setTimeout简单实现了防抖和节流功能,如果我们不考虑兼容性,追求精度比较高的页面效果,可以考虑试试html5提供的API--requestAnimationFrame。
与setTimeout相比,requestAnimationFrame的时间间隔是有系统来决定,保证屏幕刷新一次,回调函数只会执行一次,比如屏幕的刷新频率是60HZ,即间隔1000ms/60会执行一次回调。
var throttle = function (fn, delayTime) {var flag; return function () {if (!flag) {requestAnimationFrame(function () {fn(); flag = false ; }); flag = true ; } }
上述代码的基本功能就是保证在屏幕刷新的时候(对于大多数的屏幕来说,大约16.67ms),可以执行一次回调函数fn。使用这种方式也存在一种比较明显的缺点,时间间隔只能跟随系统变化,我们无法修改,但是准确性会比setTimeout高一些。
总之:
debounce:把触发非常频繁的事件(比如按键)合并成一次执行。
throttle:保证每 X 毫秒恒定的执行次数,比如每200ms检查下滚动位置,并触发 CSS 动画。
requestAnimationFrame:可替代 throttle ,函数需要重新计算和渲染屏幕上的元素时,想保证动画或变化的平滑性,可以用它。注意:IE9 不支持。
undersource.js _debounce
`_.debounce = function(func, wait, immediate) {
var timeout, result;
var later = function(context, args) {
timeout = null;
if (args) result = func.apply(context, args);
};
var debounced = restArguments(function(args) {
if (timeout) clearTimeout(timeout);
if (immediate) {
var callNow = !timeout;
timeout = setTimeout(later, wait);
if (callNow) result = func.apply(this, args);
} else {
timeout = _.delay(later, wait, this, args);
}
return result;
});
debounced.cancel = function() {
clearTimeout(timeout);
timeout = null;
};
return debounced;
};`
undersource.js _throttle
` _.throttle = function(func, wait, options) {
var timeout, context, args, result;
var previous = 0;
if (!options) options = {};
var later = function() {
previous = options.leading === false ? 0 : _.now();
timeout = null;
result = func.apply(context, args);
if (!timeout) context = args = null;
};
var throttled = function() {
var now = _.now();
if (!previous && options.leading === false) previous = now;
var remaining = wait - (now - previous);
context = this;
args = arguments;
if (remaining <= 0 || remaining > wait) {
if (timeout) {
clearTimeout(timeout);
timeout = null;
}
previous = now;
result = func.apply(context, args);
if (!timeout) context = args = null;
} else if (!timeout && options.trailing !== false) {
timeout = setTimeout(later, remaining);
}
return result;
};
throttled.cancel = function() {
clearTimeout(timeout);
previous = 0;
timeout = context = args = null;
};
return throttled;
};`
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.