阮一峰js教程总结
1、
js三种方法确定值得类型
- typeof
- instanceof
- Object.prototype.toString
null和undefined的区别
区别是这样的:
-
null 是一个表示“空”的对象,转为数值时 为 0 ;
-
-undefined 是一个表示”此处无定义”的原始值,转为数值时为 NaN 。
-
注意,空数组( [] )和空对象( {} )对应的布尔值,都是 true 。
0.1与0.3问题
0.1 + 0.2 === 0.3 2. // false
0.3 / 0.1 5. // 2.9999999999999996
(0.3 - 0.2) === (0.2 - 0.1) 8. // false
NaN
- “非数字”( NaN ),typeof NaN // ‘number’
- NaN 不等于任何值,包括它本身。
- NaN 在布尔运算时被当作 false 。
- NaN 与任何数(包括它自己)的运算,得到的都是 NaN 。
- isNaN()方法可以用来判断一个值是否为 NaN 。isNaN(NaN) // true 2. isNaN(123) // false
isNaN 只对数值有效,如果传入其他值,会被先转成数值。比如,传入字符串的时候,字符串 会被先转成 NaN ,所以最后返回 true ,这一点要特别引起注意。也就是 说, isNaN 为 true 的值,有可能不是 NaN ,而是一个字符串
对于空数组和只有一个数值成员的数组, isNaN 返回 false 。
isNaN([]) // false
isNaN([123]) // false
isNaN(['123']) // false
数值相关的全局方法
- parseInt()
- parseInt(‘1000’, 2) // 8
字符串
Base64 转码
Base64 就是一种编码方法,可以将任意值转成 0~9、A~Z、a-z、 + 和 / 这64个字符组 成的可打印字符
对象
- 对象的所有键名都是字符串(ES6 又引入了 Symbol 值也可以作为键名)
- 如果键名是数值,会被自动转为字符串。
- 查看一个对象本身的所有属性,可以使用 Object.keys 方法, Object.keys(obj);// [‘key1’, ‘key2’]
- delete 命令用于删除对象的属性,删除成功后返回 true 。能删除对象本身的属性,无法删除继承的属性
- 属性是否存在:in 运算符,自身和原型链上的都算
- 对象的 hasOwnProperty 方法判断一下,是否为对象自身的属性。
- for…in 循环用来遍历一个对象的全部属性。,对象所有可遍历(enumerable)的属性,会跳过不可遍历的属性。它不仅遍历对象自身的属性,还遍历继承的属性。
函数
- 函数参数如果是原始类型的值(数值、字符串、布尔值),传递方式是传值传递(passes by value)。这意味着,在函数体内修改参数值,不会影响到函数外部。
- 如果函数参数是复合类型的值(数组、对象、其他函数),传递方式是传址传递(pass by reference)。也就是说,传入函数的原始值的地址,因此在函数内部修改参数,将会影响到原始值。
- arguments 对象;
arguments 对象包含了函数运行时的所有参数, arguments[0] 就是第一个参 数, arguments[1] 就是第二个参数,以此类推。这个对象只有在函数体内部,才可以使用。
需要注意的是,虽然 arguments 很像数组,但它是一个对象。数组专有的方法(比 如 slice 和 forEach ),不能在 arguments 对象上直接使用。
arguments 对象使用数组方法,真正的解决方法是将 arguments 转为真正的数组。下面 是两种常用的转换方法: slice 方法和逐一填入新数组
arguments 对象带有一个 callee 属性,返回它所对应的原函数
闭包
function f1(){
var n = 999;
function f2() {
console.log(n);//999
}
}
函数 f2 就在函数 f1 内部,这时 f1 内部的所有局部变量,对 f2 都是可见的。 但是反过来就不行, f2 内部的局部变量,对 f1 就是不可见的。这就是 JavaScript 语言特有 的”链式作用域”结构(chain scope),子对象会一级一级地向上寻找所有父对象的变量。所以,父 对象的所有变量,对子对象都是可见的,反之则不成立。 既然 f2 可以读取 f1 的局部变量,那么只要把 f2 作为返回值,我们不就可以在 f1 外部读取 它的内部变量了吗!
function f1() {
var n = 999;
function f2() {
console.log(n);
}
return f2;
}
var result = f1();
result(); // 999
闭包就是函数 f2 ,即能够读取其他函数内部变量的函数。
只有函数 内部的子函数才能读取内部变量,因此可以把闭包简单理解成“定义在一个函数内部的函数”。闭包最大 的特点,就是它可以“记住”诞生的环境,比如 f2 记住了它诞生的环境 f1 ,所以从 f2 可以得 到 f1 的内部变量。在本质上,闭包就是将函数内部和函数外部连接起来的一座桥梁
闭包的最大用处有两个,
- 一个是可以读取函数内部的变量,
- 另一个就是让这些变量始终保持在内存中, 即闭包可以使得它诞生环境一直存在。
是封装对象的私有属性和私有方法。
注意,外层函数每次运行,都会生成一个新的闭包,而这个闭包又会保留外层函数的内部变量,所以内 存消耗很大。因此不能滥用闭包,否则会造成网页的性能问题。
eval
eval 命令接受一个字符串作为参数,并将这个字符串当作语句执行。
eval(‘var a = 1;’); a // 1
eval 没有自己的作用域,都在当前作用域内执行,因此可能会修改当前作用域的变量的值,造成安 全问题。
JavaScript 规定,如果使用严格模式, eval 内部声明的变量,不会影响到 外部作用域
数组
- 本质上,数组属于一种特殊的对象。 typeof 运算符会返回数组的类型是 object 。
- length 属性是可写的。如果人为设置一个小于当前成员个数的值,该数组的成员数量会自动减少 到 length 设置的值。
- 检查某个键名是否存在的运算符 in ,适用于对象,也适用于数组。
- for…in 循环不仅可以遍历对象,也可以遍历数组,毕竟数组只是一种特殊对象。
- 数组的 forEach 方法,也可以用来遍历数组
- 使用 delete 命令删除一个数组成员,会形成空位,并且不会影响 length 属性。
类似数组的对象
“类似数组的对象”的根本特征,就是具有 length 属性。只要有 length 属性,就可以认为这个对 象类似于数组。但是有一个问题,这种 length 属性不是动态值,不会随着成员的变化而变化。
典型的“类似数组的对象”是函数的 arguments 对象,以及大多数 DOM 元素集,还有字符串
典型的“类似数组的对象”是函数的 arguments 对象,以及大多数 DOM 元素集,还有字符串
var arr = Array.prototype.slice.call(arrayLike);
“类似数组的对象”还有一个办法可以使用数组的方法,就是通过 call() 把数 组的方法放到对象上面。
function print(value, index) {
console.log(index + ' : ' + value); }
Array.prototype.forEach.call(arrayLike, print);
注意,这种方法比直接使用数组原生的 forEach 要慢,所以最好还是先将“类似数组的对象”转为真 正的数组,然后再直接调用数组的 forEach 方法。
数据类型转换
错误处理机制
JavaScript 解析或运行时,一旦发生错误,引擎就会抛出一个错误对象。
JavaScript 原生提 供 Error 构造函数,所有抛出的错误都是这个构造函数的实例
var err = new Error(‘出错了’);
err.message // “出错了”
Object 对象
JavaScript 的所有其他对象都继承自 Object 对象,即那些对象都是 Object 的实例。
Object 对象的原生方法分成两类: Object 本身的方法与 Object 的实例方法。
- Object 的静态方法,是指部署在 Object 对象自身的方法。
Object.keys 方法和 Object.getOwnPropertyNames 方法都用来遍历对象的属性。
- Object 的实例方法,
- Object.prototype.hasOwnProperty() :判断某个属性是否为当前对象自身的属性,还是继承 自原型对象的属性
- Object.prototype.isPrototypeOf() :判断当前对象是否为另一个对象的原型。
- Object.prototype.propertyIsEnumerable() :判断某个属性是否可枚举。
对象的拷贝
对象的
- Object.freeze() 禁止新增删除或修改 强
- Object.seal() 只是禁止新增或删除属性 中
- Object.preventExtensions()一个对象无法再添加新的属性 弱
的可写性有一个漏洞:可以通过改变原型对象,来为对象增加属性
上面代码中,对象 obj 本身不能新增属性,但是可以在它的原型对象上新增属性,就依然能够 在 obj 上读到。
把 obj 的原型也冻结住。
var obj = new Object();
Object.preventExtensions(obj);
var proto = Object.getPrototypeOf(obj);
Object.preventExtensions(proto);
proto.t = 'hello';
obj.t // undefined
,如果属性值是对象,上面这些方法只能冻结属性指向的对象,而不能冻结对象本身的 内容。
Array.isArray()
随机数生成
function getRandomArbitrary(min, max) {
return Math.random() * (max - min) + min; 3.
}
getRandomArbitrary(1.5, 6.5)
JSON 对象
每个 JSON 对象就是一个值,可能是一个数组或对象,也可能是一个原始类型的值。总之,只能是一 个值,不能是两个或更多的值。
JSON对象是 JavaScript 的原生对象,用来处理 JSON 格式数据。它有两个静态方 法: JSON.stringify() 和 JSON.parse() 。
面向对象编程!
对象是什么
对象是单个实物的抽象。
对象是一个容器,封装了属性(property)和方法(method)
对象生成
前面说过,对象是单个实物的抽象。通常需要一个模板,表 示某一类实物的共同特征,然后对象根据这个模板生成。
- 单个对象生成 对象字面量、new Object()
- 多个对象 工厂模式、构造函数、原型模式、组合模式、寄生构造模式、稳妥模式
new 命令的作用,就是执行构造函数,返回一个实例对象
new一个对象时候,有以下四步
- 生成一个空对象
- 将作用域绑定到新对象上(this指向新对象)
- 执行构造函数(给属性上绑定方法)
- 返回对象
构造函数作为模板,可以生成实例对象。但是,有时拿不到构造函数,只能拿到一个现有的对象。我们 希望以这个现有的对象作为模板,生成新的实例对象,这时就可以使用 Object.create() 方法。
原型链
-
“原型链”(prototype chain):对象到原型,再到原型的原型…… 如果一层层地上溯,所有对象的原型最终都可以上溯到 Object.prototype ,即 Object 构造函数 的 prototype 属性。也就是说,所有对象都继承了 Object.prototype 的属性。这就是所有对象 都有 valueOf 和 toString 方法的原因,因为这是从 Object.prototype 继承的。
-
那么, Object.prototype 对象有没有它的原型呢?回答是 Object.prototype 的原型 是 null 。 null 没有任何属性和方法,也没有自己的原型。因此,原型链的尽头就是 null 。
constructor属性
prototype 对象有一个 constructor 属性,默认指向 prototype 对象所在的构造函数。
constructor 属性的作用是,可以得知某个实例对象,到底是哪一个构造函数产生的。
构造函数的继承
JavaScript 不提供多重继承功能,即不允许一个对象同时继承多个对象。但是,可以通过变通方 法,实现这个功能。
获取原型对象的三种方法
- obj.proto
- obj.constructor.prototype
- object.getPrototypeof
对象的拷贝
确保拷贝后的对象,与原对象具有同样的原型。
确保拷贝后的对象,与原对象具有同样的实例属性
异步操作
单线程模型指的是,JavaScript 只在一个线程上运行。。也就是说,JavaScript 同时只能执行一个 任务,其他任务都必须在后面排队等待
如果 JavaScript 同时有两个线程,一个线程在网页 DOM 节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准?
“事件循环”机制
如 果用得好,JavaScript 程序是不会出现堵塞的,这就是为什么 Node 可以用很少的资源,应付大流 量访问的原因
- 同步任务
同步任务是那些没有被引擎挂起、在主线程上排队执行的任务。只有前一个任务执行完毕,才能执行后 一个任务。
- 异步任务
异步任务是那些被引擎放在一边,不进入主线程、而进入任务队列的任务。只有引擎认为某个异步任务 可以执行了(比如 Ajax 操作从服务器得到了结果),该任务(采用回调函数的形式)才会进入主线 程执行。排在异步任务后面的代码,不用等待异步任务结束会马上运行,也就是说,异步任务不具 有“堵塞”效应
任务队列和事件循环
,除了一个正在运行的主线程,引擎还提供一个任务队列(task queue),里 面是各种需要当前程序处理的异步任务
异步任务的写法通常是回调函数。一旦异步任务重新进入主线程,就会执行对应的回调函数。如果一个 异步任务没有回调函数,就不会进入任务队列,也就是说,不会重新进入主线程,因为没有用回调函数 指定下一步的操作
JavaScript 引擎怎么知道异步任务有没有结果,能不能进入主线程呢?
答案就是引擎在不停地检 查,一遍又一遍,只要同步任务执行完了,引擎就会去检查那些挂起来的异步任务,是不是可以进入主 线程了。这种循环检查的机制,就叫做事件循环(Event Loop)。
异步操作的模式
-
回调函数
回调函数的优点是简单、容易理解和实现,缺点是不利于代码的阅读和维护,各个部分之间高度耦合) (coupling),使得程序结构混乱、流程难以追踪(尤其是多个回调函数嵌套的情况),而且每个任 务只能指定一个回调函数。 -
事件监听
-发布/订阅 事件完全可以理解成“信号”,如果存在一个“信号中心”,某个任务执行完成,就向信号中心“发 布”(publish)一个信号,其他任务可以向信号中心“订阅”(subscribe)这个信号,从而知道什么 时候自己可以开始执行。这就叫做”发布/订阅模式”(publish-subscribe pattern),又称“观察 者模式”(observer pattern)。
异步操作的流程控制
如果有多个异步操作,就存在一个流程控制的问题:如何确定异步操作执行的顺序,以及如何保证遵守 这种顺序。
Promise对象
首先,Promise 是一个对象,也是一个构造函数。
实例,使用Promise完成图片加载
var preloadImage = function(path) {
return new Promise(function (resolve,reject){
var image = new Image();
image.onload = resolve;
image.onerror = reject;
image.src = path;});
}
上面代码中, image 是一个图片对象的实例。它有两个事件监听属性, onload 属性在图片加载成 功后调用, onerror 属性在加载失败调用。
而且,Promise 还有一个传统写法没有的好处:它的状态一旦改变,无论何时查询,都能得到这个状 态。这意味着,无论何时为 Promise 实例添加回调函数,该函数都能正确执行。所以,你不用担心是 否错过了某个事件或信号。如果是传统写法,通过监听事件来执行回调函数,一旦错过了事件,再添加 回调函数是不会执行的。
微任务
DOM
DOM 是 JavaScript 操作网页的接口,全称为“文档对象模型”(Document Object Model)。它 的作用是将网页转为一个 JavaScript 对象,从而可以用脚本进行各种操作(比如增删内容)。
浏览器会根据 DOM 模型,将结构化文档(比如 HTML 和 XML)解析成一系列的节点,再由这些节点 组成一个树状结构(DOM Tree)。所有的节点和最终的树状结构,都有规范的对外接口。
第一个是文档类型节点( <!doctype html> ),
第二个是 HTML 网页的 顶层容器标签
三种层级关系
- 父节点关系(parentNode):直接的那个上级节点
- 子节点关系(childNodes):直接的下级节点
- 同级节点关系(sibling):拥有同一个父节点的节点
DOM 提供操作接口,用来获取这三种关系的节点。比如,子节点接口包括 firstChild (第一个子节 点)和 lastChild (最后一个子节点)等属性,同级节点接口包括 nextSibling (紧邻在后的那 个同级节点)和 previousSibling (紧邻在前的那个同级节点)属性。
Document 节点
document.location Location 对象是浏览器提供的原生对象,提供 URL 相关的信息和操作方法。通 过 window.location 和 document.location 属性,可以拿到这个对象。
Element 节点
Element 节点对象对应网页的 HTML 元素。每一个 HTML 元素,在 DOM 树上都会转化成一 个 Element 节点对象(以下简称元素节点)。
EventTarget 接口
addEventListener :绑定事件的监听函数
removeEventListener :移除事件的监听函数
dispatchEvent :触发事件
function hello() {
console.log('Hello world');
}
var button = document.getElementById('btn');
button.addEventListener('click', hello, false);
事件模型
监听函数
浏览器的事件模型,就是通过监听函数(listener)对事件做出反应。事件发生后,浏览器监听到了 这个事件,就会执行对应的监听函数。这是事件驱动编程模式(event-driven)的主要编程方式。
有三种方法绑定事件监听函数
HTML 的 on- 属性
元素节点的事件属性
window.onload = doSomething;
div.onclick = function (event) {
console.log('触发事件');
};
EventTarget.addEventListener()
所有 DOM 节点实例都有 addEventListener 方法,用来为该节点定义事件的监听函数。
this 的指向
监听函数内部的 this 指向触发事件的那个元素节点。
事件的传播
一个事件发生后,会在子元素和父元素之间传播(propagation)。这种传播分成三个阶段。
- 第一阶段:从 window 对象传导到目标节点(上层传到底层),称为“捕获阶段”(capture phase)。
- 第二阶段:在目标节点上触发,称为“目标阶段”(target phase)。
- 第三阶段:从目标节点传导回 window 对象(从底层传回上层),称为“冒泡阶段”(bubbling phase)。
事件的代理
由于事件会在冒泡阶段向上传播到父节点,因此可以把子节点的监听函数定义在父节点上,由父节点的 监听函数统一处理多个子元素的事件。这种方法叫做事件的代理(delegation)
var ul = document.querySelector('ul');
ul.addEventListener('click', function (event) {
if (event.target.tagName.toLowerCase() === 'li') {
// some code
}
});
Event 对象
事件发生以后,会产生一个事件对象,作为参数传给监听函数。浏览器原生提供一个 Event 对象,所 有的事件都是这个对象的实例,或者说继承了 Event.prototype 对象。
Event 对象本身就是一个构造函数,可以用来生成新的实例。
Event.preventDefault()
Event.stopPropagation() ,
因此,任意时点都有两个与事 件相关的节点,一个是事件的原始触发节点( Event.target ),另一个是事件当前正在通过的节点 ( Event.currentTarget )。前者通常是后者的后代节点
**Event.target 属性返回原始触发事件的那个节点,即事件最初发生的节点。**这个属性不会随着事件 的传播而改变。
网页状态事件
DOMContentLoaded 事件
网页下载并解析完成以后,浏览器就会在 document 对象上触发 DOMContentLoaded 事件。这 时,仅仅完成了网页的解析(整张页面的 DOM 生成了),所有外部资源(样式表、脚本、iframe 等 hashchange 事件 网页状态事件 DOMContentLoaded 事件 其他常见事件 - 580 - 本文档使用 书栈网 · BookStack.CN 构建
等)可能还没有下载结束。也就是说,这个事件比 load 事件,发生时间早得多。
注意,网页的 JavaScript 脚本是同步执行的,脚本一旦发生堵塞,将推迟触 发 DOMContentLoaded 事件。
script
正常的网页加载流程是这样的。
-
浏览器一边下载 HTML 网页,一边开始解析。也就是说,不等到下载完,就开始解析。
-
解析过程中,浏览器发现
加载外部脚本时,浏览器会暂停页面渲染,等待脚本下载并执行完成后,再继续渲染。原因是 JavaScript 代码可以修改 DOM,所以必须把控制权让给它,否则会导致复杂的线程竞赛的问题
如果外部脚本加载时间很长(一直无法完成下载),那么浏览器就会一直等待脚本下载完成,造成网页 长时间失去响应,浏览器就会呈现“假死”状态,这被称为“阻塞效应”。
- 将
这样即使遇到脚 本失去响应,网页主体的渲染也已经完成了,用户至少可以看到内容,而不是面对一张空白的页面。如 果某些脚本代码非常重要,一定要放在页面头部的话,最好直接将代码写入页面,而不是连接外部脚本 文件,这样能缩短加载时间。
脚本文件都放在网页尾部加载,还有一个好处,在 DOM 结构生成之前就调用 DOM 节点, JavaScript 会报错,如果脚本都在网页尾部加载,就不存在这个问题,因为这时 DOM 肯定已经生 成了
一种解决方法是设定 DOMContentLoaded 事件的回调函数。
另一种解决方法是,使用
多个script
浏览器会同时并行下载 a.js 和 b.js ,但是,执行时会保证先执行 a.js ,然后再执 行 b.js ,即使后者先下载完成,
也就是说,脚本的执行顺序由它们在页面中的出现顺序 决定,这是为了保证脚本之间的依赖关系不受到破坏。当然,加载这两个脚本都会产生“阻塞效应”,必 须等到它们都加载完成,浏览器才会继续页面渲染。
解析和执行 CSS,也会产生阻塞。Firefox 浏览器会等到脚本前面的所有样式表,都下载并解析完, 浏览器模型概述 - 596 - 本文档使用 书栈网 · BookStack.CN 构建
再执行脚本;Webkit则是一旦发现脚本引用了样式,就会暂停执行脚本,等到样式表下载并解析完, 再恢复执行
defer
为了解决脚本文件下载阻塞网页渲染的问题,一个方法是对
只有等到 DOM 加载完成后,才会执行 a.js 和 b.js 。
defer 属性的运行流程如下
- 浏览器开始解析 HTML 网页
- 解析过程中,发现带有 defer 属性的
有了 defer 属性,浏览器下载脚本文件的时候,不会阻塞页面渲染。下载的脚本文件 在 DOMContentLoaded 事件触发前执行(即刚刚读取完 标签),而且可以保证执行顺序 就是它们在页面上出现的顺序。
对于内置而不是加载外部脚本的 script 标签,以及动态生成的 script 标签, defer 属性不起 作用。另外,使用 defer 加载的外部脚本不应该使用 document.write 方法。
async
解决“阻塞效应”的另一个方法是对
async 属性的作用是,使用另一个进程下载脚本,下载时不会阻塞渲染。
- 浏览器开始解析 HTML 网页。
- 解析过程中,发现带有 async 属性的 script 标签。 defer 属性 async 属性
- 浏览器继续往下解析 HTML 网页,同时并行下载
async 属性可以保证脚本下载的同时,浏览器继续渲染。需要注意的是,一旦采用这个属性,就无法 保证脚本的执行顺序。哪个脚本先下载结束,就先执行那个脚本
defer 属性和 async 属性到底应该使用哪一个?
一般来说,如果脚本之间没有依赖关系,就使用 async 属性,如果脚本之间有依赖关系,就使 用 defer 属性。如果同时使用 async 和 defer 属性,后者不起作用,浏览器行为 由 async 属性决定。