原生JS高级-中
原生JS高级-中
一、对象创建模式
1.1 Object构造函数模式
步骤:先创建一个空 Object 对象,再添加属性和方法。
适用场景:起始时不确定对象内部数据。
问题:语句太多。
var p1 = new Object();
p1.name = '张三';
p1.age = 22;
p1.dowork = function(){
console.log("正在工作中");
}
1.2 对象字面量模式
步骤:使用 { } 来创建对象,同时指定属性和方法。
适用场景:起始时对象内部数据是确定的。
问题:如果创建多个对象,就会有重复代码。
var p1 = {
name: "张三",
age: 22,
dowork: function () {
console.log("正在工作中");
}
}
1.3 工厂模式
步骤:通过工厂函数动态创建对象并返回。
适用场景:需要创建多个相似对象。
问题:对象没有一个具体的类型,都是 Object 类型。
function createPerson(name, age, dowork) {
var obj = {
name: name,
age: age,
dowork: dowork
}
return obj;
}
var p1 = createPerson("张三", 22,
function () { console.log("正在工作中"); });
1.4 自定义构造函数模式
步骤:自定义构造函数,通过 new 创建对象。
适用场景:需要创建多个类型确定的对象。
问题:每个对象会有相同的数据(属性 / 函数),浪费内存。
function Person(name, age, dowork) {
this.name = name;
this.age = age;
this.dowork = dowork;
}
var p1 = new Person("张三", 22, function () { console.log("正在工作中"); });
var p2 = new Person("李四", 20, function () { console.log("正在工作中"); });
console.log(p1, p2);
下面展示输出结果,可以看到两个对象中的 dowork 属性已经重复了。

1.5 构造函数加上原型的组合模式
我们将上面的 dowork 属性放入到原型里,就不会重复定义了。而每一个实例对象都可以访问原型里的函数。
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.dowork = function () { console.log("正在工作中"); }
var p1 = new Person("张三", 22);
var p2 = new Person("李四", 20);
// 都可以正常输出
console.log(p1.dowork, p2.dowork);
二、继承模式
我们可以利用原型链的特性来实现继承;也可以使用借用构造函数继承。
我们现在需要将这两个模式结合起来。
我们使用 父函数.call(this, 父函数参数) 来实现继承父类的操作与 Java 的 super(); 一致。
第 7 行:调用父函数,使学生也可以初始化名字 name 和年龄 age 。
function Person(name, age) {
this.name = name;
this.age = age;
}
function Student(stuId, name, age) {
Person.call(this, name, age);
this.stuId = stuId;
}
然后但我们现在并不能调用 Person 原型里的方法,我们需要将 Student 的原型与 Person 的原型对接起来。
我们将使用 Object.create(目标原型) 方法,其创建一个新对象,使用现有的对象来提供新创建的对象的 __proto__ 。简单来说,就是使一个对象的隐式原型指向那个参数的对象。
Student.prototype = Object.create(Person.prototype); 这句代码就是让 Student 的显式原型的隐式原型指向 Person 的显式原型。与这句代码差不多的效果 Student.prototype = new Person(); 。
使用两个方法,再输出 Student 函数的显式原型看一看。
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.getName = function () {
return this.name;
}
function Student(stuId, name, age) {
Person.call(this, name, age);
this.stuId = stuId;
}
// Student.prototype = new Person();
Student.prototype = Object.create(Person.prototype);
console.log(Student);

左边的是使用构造函数,右边是使用 create 方法。
使用构造函数来创建 Student 的原型对象,会出现一些不需要的属性,这个例子为 name 和 age。而右边却没有不需要的属性,所以使用 create 方法较好。
但两个方式都没有 constructor 属性,就像箭头指的那个。我们需要手动设置。
Student.prototype.constructor = Student;
然后最后的代码及测试如下:
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.getName = function () {
return this.name;
}
function Student(stuId, name, age) {
Person.call(this, name, age);
this.stuId = stuId;
}
Student.prototype = Object.create(Person.prototype);
Student.prototype.constructor = Student;
var stu1 = new Student("123", "张三", 22);
// 调用父函数的方法
var name = stu1.getName();
// 成功返回 "张三"
console.log(name);
三、线程机制
3.1 区分进程和线程
(1)概念
在操作系统中有官方的提到过这两个概念:
- 进程是cpu 资源分配的最小单位(是能拥有资源和独立运行的最小单位)
- 线程是cpu 调度的最小单位(线程是建立在进程的基础上的一次程序运行单位,一个进程中可以有多个线程)
(2)浏览器中的进程
浏览器是多进程的,每打开一个Tab页,就相当于创建了一个独立的浏览器进程。
浏览器的进程不单单只有 Tab 页,还有多种进程:
- Browser 进程:浏览器的主进程(负责协调、主控),只有一个。作用有:
- 负责浏览器界面显示,与用户交互。如前进,后退等;
- 负责各个页面的管理,创建和销毁其他进程;
- 将 Renderer 进程得到的内存中的Bitmap,绘制到用户界面上;
- 网络资源的管理,下载等。
- 第三方插件进程:每种类型的插件对应一个进程,仅当使用该插件时才创建;
- GPU进程:最多一个,用于3D绘制等;
- 浏览器渲染进程(浏览器内核)(Renderer进程,内部是多线程的):默认每个Tab页面一个进程,互不影响。主要作用为:
- 页面渲染,脚本执行,事件处理等。
具体的可以打开 Chrome 里的任务管理器查看,下图为 Chrome 任务管理器的每个任务的标识。

(3)浏览器多进程的优势
相比于单进程浏览器,多进程有如下优点:
- 避免单个 页面崩溃影响整个浏览器;
- 避免第三方插件崩溃影响整个浏览器;
- 多进程充分利用多核优势;
- 方便使用沙盒模型隔离插件等进程,提高浏览器稳定性。
简单点理解:如果浏览器是单进程,那么某个页面崩溃了,就影响了整个浏览器或者其他的页面;同理如果是单进程,插件崩溃了也会影响到浏览器或者其他的页面。
因为是单进程,所以如果当前进程的内存中一个内容发生错误,进程中的所有内容都需要重载,也就是说你打开的所有页面都会从网络上重新请求一次。
当然,内存等资源消耗也会更大,有点空间换时间的意思。
3.2 循环队列
- JS 分为 同步任务 和 异步任务。
- 同步任务都在主线程上执行,形成一个 执行栈,类似于执行上下文栈。
- 主线程之外,事件触发线程 管理着一个 任务队列,只要异步任务有了运行结果,就在任务队列之中放置一个事件。
- 一旦 执行栈 中的所有同步任务执行完毕(此时 JS 引擎空闲),系统就会读取任务队列,将可运行的异步任务添加到可执行栈中,开始执行。
- 等到可执行栈的任务执行完毕之后,再将任务队列里的任务继续添加到可执行栈中,如此循环。

3.3 浏览器的渲染进程
浏览器的渲染进程是多线程的 ,这里的渲染进程指的是页面的总进程。
而渲染进程包含了哪些线程(列举一些主要常驻线程):
- GUI渲染线程
- 负责渲染浏览器界面,解析HTML,CSS,构建DOM树和RenderObject树,布局和绘制等。
- 当界面需要重绘(Repaint)或由于某种操作引发回流(reflow)时,该线程就会执行。
- 注意,GUI渲染线程与JS引擎线程是互斥的,当 JS 引擎执行时 GUI 线程会被挂起(相当于被冻结了),GUI更新会被保存在一个队列中 等到JS引擎空闲时 立即被执行。
- JS 引擎线程
- 也称为 JS 内核,负责处理、解析和运行 Javascript 脚本程序。(例如 V8 引擎)
- JS引擎一直等待着任务队列中任务的到来,然后加以处理,一个Tab页(renderer 进程)中无论什么时候都只有一个JS线程在运行JS程序
- 同样注意,GUI渲染线程与JS引擎线程是互斥的,所以如果 JS 执行的时间过长,这样就会造成页面的渲染不连贯,导致页面渲染加载阻塞。
- 事件触发线程
- 归属于浏览器而不是 JS 引擎(可以理解,JS引擎自己都忙不过来,需要浏览器另开线程协助)
- 当 JS 引擎执行代码块,例如初始化 setTimeOut 、添加鼠标点击监听、AJAX异步请求,会将对应任务添加到事件线程中。
- 当对应的 事件符合触发条件被触发 时,该线程会把事件 添加到循环队列的队尾,等待 JS 引擎的处理。
- 定时触发器线程
- 传说中的
setInterval与setTimeout所在线程。 - 浏览器定时计数器并不是由 JavaScript 引擎计数的。(因为JavaScript引擎是单线程的, 如果处于阻塞线程状态就会影响记计时的准确)
- 因此通过 单独线程来计时并触发定时(计时完毕后,将回调函数添加到 事件队列 中,等待JS引擎空闲后执行)
- 注意,W3C 在 HTML 标准中规定,规定要求 setTimeout 中低于 4ms 的时间间隔算为4ms。
- 传说中的
- 异步 http 请求线程
- 在 XMLHttpRequest 在连接后是通过浏览器新开一个线程请求。
- 将检测到状态变更时,如果设置有回调函数,异步线程就 产生状态变更事件,将这个回调再放入循环队列中。再由 JavaScript 引擎执行。

3.4 通信过程
之前有提到 Browser 进程,浏览器的主进程,但每一个浏览器渲染进程是如何与主进程通信的呢?
- Browser 进程收到用户请求,首先需要 获取页面内容(譬如通过网络下载资源),随后将该任务 通过RendererHost 接口传递给 Render 进程。
- Renderer 进程的 Renderer 接口收到消息,简单解释后,交给渲染线程,然后开始渲染:
- 渲染线程接收请求,加载网页并渲染网页,这其中可能需要 Browser 进程获取资源和需要 GPU 进程来帮助渲染。
- 当然可能会有 JS 线程操作 DOM(这样可能会造成回流并重绘)
- 最后 Render 进程将结果传递给 Browser 进程
- Browser 进程 接收到结果并将结果绘制出来。

3.5 浏览器渲染流程
最开始的几步:
浏览器输入 url,浏览器主进程接管,开一个下载线程,
然后进行 http请求(略去DNS查询,IP寻址等等操作),然后等待响应,获取内容。
随后将内容通过 RendererHost 接口转交给 Renderer 进程
浏览器渲染流程开始,也就是上图的右下角模块的执行流程。
下面为渲染的流程:
- 解析 HTML 建立 DOM 树。
- 将 CSS 代码解析成树形的数据结构——Style 树,然后与 DOM 树合并,形成 Render 树。
- 布局 Render 树(Layout/reflow),负责各元素尺寸、位置的计算。
- 绘制 Render 树(paint),绘制页面像素信息。
- 浏览器会将 各层的信息 发送给GPU,GPU会将各层合成(composite),显示在屏幕上。

渲染完毕后就是 window.onload 事件了。这里还有一个概念 DOMContentLoaded 事件。
- 当 DOMContentLoaded 事件触发时,仅当DOM加载完成,不包括样式表,图片。与 jQuery 的
$(回调函数)函数触发的时机一致。 - 当 onload 事件触发时,页面上所有的DOM,样式表,脚本,图片都已经加载完成了。
也就是说 DOMContentLoaded 事件先于 onload 事件。