ECMAScript 核心概念
2020-04-16 11:08:12 1 举报
AI智能生成
JavaScript 核心概念
作者其他创作
大纲/内容
原型链与继承
当谈到继承时,JavaScript 只有一种结构:对象。
每个实例对象( object )都有一个私有属性(称之为 __proto__ )指向它的原型对象。
该对象也有一个自己的原型对象 ,层层向上直到一个对象的原型对象为 null。
根据定义,null 没有原型,并作为这个原型链中的最后一个环节。
几乎所有 JavaScript 中的对象都是位于原型链顶端的 Object 的实例。
每个实例对象( object )都有一个私有属性(称之为 __proto__ )指向它的原型对象。
该对象也有一个自己的原型对象 ,层层向上直到一个对象的原型对象为 null。
根据定义,null 没有原型,并作为这个原型链中的最后一个环节。
几乎所有 JavaScript 中的对象都是位于原型链顶端的 Object 的实例。
someObject.[[prototype]] vs someFunc.prototype
被构造函数创建的实例对象的 [[prototype]] 指向构造函数的 prototype 属性
基于原型链的继承
继承属性
JavaScript 对象有一个指向一个原型对象的链。当试图访问一个对象的属性时,它不仅仅在该对象上搜寻,还会搜寻该对象的原型,以及该对象的原型的原型,依次层层向上搜索,直到找到一个名字匹配的属性或到达原型链的末尾。
继承方法
当继承的函数被调用时,this 指向的是当前继承的对象,而不是继承的函数所在的原型对象。
在 JavaScript 中使用原型
JavaScript 中所有的 非箭头函数 都有一个特别的 prototype 属性
function someFn() {}
console.log(someFn.prototype);
// 和声明函数的方式无关,
// JavaScript 中的非箭头函数永远有一个默认 prototype 属性。
var someFn = function() {};
console.log(someFn.prototype);
function someFn() {}
console.log(someFn.prototype);
// 和声明函数的方式无关,
// JavaScript 中的非箭头函数永远有一个默认 prototype 属性。
var someFn = function() {};
console.log(someFn.prototype);
不同的方法来创建的对象及其生成的原型链
使用 字面量 创建的对象
var o = {a: 1};
o 这个对象继承了 Object.prototype 上面的所有属性
原型链如下:
o ---> Object.prototype ---> null
原型链如下:
o ---> Object.prototype ---> null
var a = ["yo", "whadup", "?"];
数组都继承于 Array.prototype
(Array.prototype 中包含 indexOf, forEach 等方法)
原型链如下:
a ---> Array.prototype ---> Object.prototype ---> null
(Array.prototype 中包含 indexOf, forEach 等方法)
原型链如下:
a ---> Array.prototype ---> Object.prototype ---> null
function f(){
return 2;
}
return 2;
}
函数都继承于 Function.prototype
(Function.prototype 中包含 call, bind等方法)
原型链如下:
f---> Function.prototype ---> Object.prototype ---> null
(Function.prototype 中包含 call, bind等方法)
原型链如下:
f---> Function.prototype ---> Object.prototype ---> null
使用构造器创建的对象
function Graph() {
this.vertices = [];
this.edges = [];
}
Graph.prototype = {
addVertex: function(v){
this.vertices.push(v);
}
};
var g = new Graph();
// g 是生成的对象,他的自身属性有 'vertices' 和 'edges'。
// 在 g 被实例化时,g.[[Prototype]] 指向了 Graph.prototype。
this.vertices = [];
this.edges = [];
}
Graph.prototype = {
addVertex: function(v){
this.vertices.push(v);
}
};
var g = new Graph();
// g 是生成的对象,他的自身属性有 'vertices' 和 'edges'。
// 在 g 被实例化时,g.[[Prototype]] 指向了 Graph.prototype。
当你执行:
var o = new Foo();
JavaScript 实际上执行的是:
var o = new Object();
o.__proto__ = Foo.prototype;
Foo.call(o);
var o = new Foo();
JavaScript 实际上执行的是:
var o = new Object();
o.__proto__ = Foo.prototype;
Foo.call(o);
使用 Object.create 创建的对象
可以调用这个方法来创建一个新对象。新对象的原型就是调用 create 方法时传入的第一个参数。
var a = {a: 1};
// a ---> Object.prototype ---> null
var b = Object.create(a);
// b ---> a ---> Object.prototype ---> null
console.log(b.a); // 1 (继承而来)
var c = Object.create(b);
// c ---> b ---> a ---> Object.prototype ---> null
var d = Object.create(null);
// d ---> null
// a ---> Object.prototype ---> null
var b = Object.create(a);
// b ---> a ---> Object.prototype ---> null
console.log(b.a); // 1 (继承而来)
var c = Object.create(b);
// c ---> b ---> a ---> Object.prototype ---> null
var d = Object.create(null);
// d ---> null
使用 class 关键字创建的对象
性能
在原型链上查找属性比较耗时,对性能有副作用,这在性能要求苛刻的情况下很重要。
遍历对象的属性时,原型链上的每个可枚举属性都会被枚举出来。要检查对象是否具有自己定义的属性,
而不是其原型链上的某个属性,则必须使用所有对象从 Object.prototype 继承的 hasOwnProperty 方法。
hasOwnProperty 是 JavaScript 中唯一一个处理属性并且不会遍历原型链的方法。
遍历对象的属性时,原型链上的每个可枚举属性都会被枚举出来。要检查对象是否具有自己定义的属性,
而不是其原型链上的某个属性,则必须使用所有对象从 Object.prototype 继承的 hasOwnProperty 方法。
hasOwnProperty 是 JavaScript 中唯一一个处理属性并且不会遍历原型链的方法。
其他相关知识
instanceof 运算符
instanceof 运算符用来检测 constructor.prototype 是否存在于参数 object 的原型链上。
object instanceof constructor
手写
function myInstanceOf(obj,cst){
let P=cst.prototype
let objP = Object.getPrototypeOf(obj);
while(objP){
if(objP===P){
return true
}
objP = Object.getPrototypeOf(objP);
}
return false
}
let P=cst.prototype
let objP = Object.getPrototypeOf(obj);
while(objP){
if(objP===P){
return true
}
objP = Object.getPrototypeOf(objP);
}
return false
}
屏蔽属性
var a = {num:2};
var b = Object.create(a);
//问题,以下顺序执行,值是?
b.num
b.num++
a.num
var b = Object.create(a);
//问题,以下顺序执行,值是?
b.num
b.num++
a.num
1. b.num ==> 2
Object.create()方法创建一个新对象,
使用现有的对象来提供新创建的对象的__proto__。
这里 b.__proto__ == a
所以虽然b是个空对象,但会从原型上一直找值。
b.num == b.__proto__.num == 2
使用现有的对象来提供新创建的对象的__proto__。
这里 b.__proto__ == a
所以虽然b是个空对象,但会从原型上一直找值。
b.num == b.__proto__.num == 2
2. b.num++ ==> 2
——《你不知道的javascript·上卷》P144
分析一下如果 foo 不直接存在于 myObject 中而是存在于原型链上层时 myObject.foo = "bar" 会出现的三种情况。
1. 如果在[[Prototype]]链上层存在名为foo的普通数据访问属性(参见第3章)并且没有被标记为只读(writable:false),
那就会直接在 myObject 中添加一个名为 foo 的新属性,它是屏蔽属性。
2. 如果在[[Prototype]]链上层存在foo,但是它被标记为只读(writable:false),那么无法修改已有属性或者在 myObject 上创建屏蔽属性。
如果运行在严格模式下,代码会抛出一个错误。否则,这条赋值语句会被忽略。总之,不会发生屏蔽。
3. 如果在[[Prototype]]链上层存在foo并且它是一个setter(参见第3章),那就一定会调用这个 setter。
foo 不会被添加到(或者说屏蔽于)myObject,也不会重新定义 foo 这 个 setter。
大多数开发者都认为如果向 [[Prototype]] 链上层已经存在的属性([[Put]])赋值,就一 定会触发屏蔽,但是如你所见,
三种情况中只有一种(第一种)是这样的。
如果你希望在第二种和第三种情况下也屏蔽 foo,那就不能使用 = 操作符来赋值,而是使用 Object.defineProperty(..)
(参见第 3 章)来向 myObject 添加 foo。
分析一下如果 foo 不直接存在于 myObject 中而是存在于原型链上层时 myObject.foo = "bar" 会出现的三种情况。
1. 如果在[[Prototype]]链上层存在名为foo的普通数据访问属性(参见第3章)并且没有被标记为只读(writable:false),
那就会直接在 myObject 中添加一个名为 foo 的新属性,它是屏蔽属性。
2. 如果在[[Prototype]]链上层存在foo,但是它被标记为只读(writable:false),那么无法修改已有属性或者在 myObject 上创建屏蔽属性。
如果运行在严格模式下,代码会抛出一个错误。否则,这条赋值语句会被忽略。总之,不会发生屏蔽。
3. 如果在[[Prototype]]链上层存在foo并且它是一个setter(参见第3章),那就一定会调用这个 setter。
foo 不会被添加到(或者说屏蔽于)myObject,也不会重新定义 foo 这 个 setter。
大多数开发者都认为如果向 [[Prototype]] 链上层已经存在的属性([[Put]])赋值,就一 定会触发屏蔽,但是如你所见,
三种情况中只有一种(第一种)是这样的。
如果你希望在第二种和第三种情况下也屏蔽 foo,那就不能使用 = 操作符来赋值,而是使用 Object.defineProperty(..)
(参见第 3 章)来向 myObject 添加 foo。
——《你不知道的javascript·上卷》P161
尽管 myObject.a++ 看起来应该(通过委托)查找并增加 anotherObject.a 属性,
但是别忘 了 ++ 操作相当于 myObject.a = myObject.a + 1。
因此 ++ 操作首先会通过 [[Prototype]] 查找属性 a 并从 anotherObject.a 获取当前属性值 2,
然后给这个值加 1,接着用 [[Put]] 将值 3 赋给 myObject 中新建的屏蔽属性 a 。
尽管 myObject.a++ 看起来应该(通过委托)查找并增加 anotherObject.a 属性,
但是别忘 了 ++ 操作相当于 myObject.a = myObject.a + 1。
因此 ++ 操作首先会通过 [[Prototype]] 查找属性 a 并从 anotherObject.a 获取当前属性值 2,
然后给这个值加 1,接着用 [[Put]] 将值 3 赋给 myObject 中新建的屏蔽属性 a 。
b.num为2,后置++运算符是先用后加,这时b.num++返回还是2
3. a.num ==> 2
b.num 在执行 ++ 操作之后,这时的b为{num:3},不影响a.
深拷贝
考虑点
1. 循环引用
// 下面的几种目前没有搜到哪个方法会改变它们自身,算可选吧
// 不过可能某些特殊情况会需要对下面的类型也进行严格的深拷贝
2. new String('ss')
3. dom
4. date
// 下面的几种目前没有搜到哪个方法会改变它们自身,算可选吧
// 不过可能某些特殊情况会需要对下面的类型也进行严格的深拷贝
2. new String('ss')
3. dom
4. date
木易杨的处理循环引用的核心方法 clone(source, uniqueList),有动态规划的思想
代码
测试用例:
let toCopy = {
name:'toCopy',
str: new String("hello"),
dom: document.createElement("div"),
date:new Date(),
arr: [
{
name: "ele-0",
number: new Number("100"),
obj: {
name:'ele-0-obj'
}
}
],
cc:null
};
let cc = {
name: "cc",
circleRef: toCopy
};
toCopy.cc = cc;
let copy = cloneDeep(toCopy);
console.log(copy);
let toCopy = {
name:'toCopy',
str: new String("hello"),
dom: document.createElement("div"),
date:new Date(),
arr: [
{
name: "ele-0",
number: new Number("100"),
obj: {
name:'ele-0-obj'
}
}
],
cc:null
};
let cc = {
name: "cc",
circleRef: toCopy
};
toCopy.cc = cc;
let copy = cloneDeep(toCopy);
console.log(copy);
function cloneDeep(source) {
const types = [Number, String, Boolean];
function find(arr, item) {
for (var i = 0; i < arr.length; i++) {
if (arr[i].source === item) {
return arr[i];
}
}
return null;
}
function clone(source, uniqueList) {
if (!source) {
return source;
}
let uniqueData = find(uniqueList, source);
if (uniqueData) {
return uniqueData.target;
}
let curUnique = {
source: source,
target: undefined
};
uniqueList.push(curUnique);
// normalizing primitives if someone did new String('aaa'), or new Number('444');
types.forEach(function(type) {
if (source instanceof type) {
curUnique.target = type(source);
}
});
if (typeof curUnique.target == "undefined") {
if (Object.prototype.toString.call(source) === "[object Array]") {
curUnique.target = [];
source.forEach(function(child, index, array) {
curUnique.target[index] = clone(child, uniqueList);
});
} else if (typeof source == "object") {
// DOM
if (source.nodeType && typeof source.cloneNode == "function") {
curUnique.target = source.cloneNode(true);
} else {
// Date
if (source instanceof Date) {
curUnique.target = new Date(source);
} else {
// it is an object literal
curUnique.target = {};
for (let key in source) {
// 这里一定要用上 Object.prototype.hasOwnProperty
if (Object.prototype.hasOwnProperty.call(source,key)) {
curUnique.target[key] = clone(source[key],uniqueList);
}
}
}
}
} else {
curUnique.target = source;
}
}
return curUnique.target;
}
return clone(source, []);
}
const types = [Number, String, Boolean];
function find(arr, item) {
for (var i = 0; i < arr.length; i++) {
if (arr[i].source === item) {
return arr[i];
}
}
return null;
}
function clone(source, uniqueList) {
if (!source) {
return source;
}
let uniqueData = find(uniqueList, source);
if (uniqueData) {
return uniqueData.target;
}
let curUnique = {
source: source,
target: undefined
};
uniqueList.push(curUnique);
// normalizing primitives if someone did new String('aaa'), or new Number('444');
types.forEach(function(type) {
if (source instanceof type) {
curUnique.target = type(source);
}
});
if (typeof curUnique.target == "undefined") {
if (Object.prototype.toString.call(source) === "[object Array]") {
curUnique.target = [];
source.forEach(function(child, index, array) {
curUnique.target[index] = clone(child, uniqueList);
});
} else if (typeof source == "object") {
// DOM
if (source.nodeType && typeof source.cloneNode == "function") {
curUnique.target = source.cloneNode(true);
} else {
// Date
if (source instanceof Date) {
curUnique.target = new Date(source);
} else {
// it is an object literal
curUnique.target = {};
for (let key in source) {
// 这里一定要用上 Object.prototype.hasOwnProperty
if (Object.prototype.hasOwnProperty.call(source,key)) {
curUnique.target[key] = clone(source[key],uniqueList);
}
}
}
}
} else {
curUnique.target = source;
}
}
return curUnique.target;
}
return clone(source, []);
}
我写的用 Map 数据结构
来保存唯一副本的版本
来保存唯一副本的版本
function cloneDeep(source) {
const types = [Number, String, Boolean];
function clone(source, uniqueMap) {
if (!source) {
return;
}
let uniqueTarget = uniqueMap.get(source);
if (uniqueTarget) {
return uniqueTarget;
}
let curTarget;
// normalizing primitives if someone did new String('aaa'), or new Number('444');
types.forEach(function(type) {
if (source instanceof type) {
curTarget = type(source);
uniqueMap.set(source, curTarget);
// 在 forEach 回调函数里面的 return 并不能让外面的 clone 函数 return
// return curTarget;
}
});
// 如果是上面的情况,就返回结果
if(curTarget) return curTarget
if (Object.prototype.toString.call(source) === "[object Array]") {
curTarget = [];
uniqueMap.set(source, curTarget);
source.forEach(function(child, index, array) {
curTarget[index] = clone(child, uniqueMap);
});
} else if (typeof source == "object") {
// DOM
if (source.nodeType && typeof source.cloneNode == "function") {
curTarget = source.cloneNode(true);
uniqueMap.set(source, curTarget);
} else {
// Date
if (source instanceof Date) {
curTarget = new Date(source);
uniqueMap.set(source, curTarget);
} else {
// it is an object literal
curTarget = {};
uniqueMap.set(source, curTarget);
for (let key in source) {
// 这里一定要用上 Object.prototype.hasOwnProperty.call
if (Object.prototype.hasOwnProperty.call(source, key)) {
curTarget[key] = clone(source[key], uniqueMap);
}
}
}
}
} else {
curTarget = source;
uniqueMap.set(source, curTarget);
}
return curTarget;
}
return clone(source, new Map());
}
const types = [Number, String, Boolean];
function clone(source, uniqueMap) {
if (!source) {
return;
}
let uniqueTarget = uniqueMap.get(source);
if (uniqueTarget) {
return uniqueTarget;
}
let curTarget;
// normalizing primitives if someone did new String('aaa'), or new Number('444');
types.forEach(function(type) {
if (source instanceof type) {
curTarget = type(source);
uniqueMap.set(source, curTarget);
// 在 forEach 回调函数里面的 return 并不能让外面的 clone 函数 return
// return curTarget;
}
});
// 如果是上面的情况,就返回结果
if(curTarget) return curTarget
if (Object.prototype.toString.call(source) === "[object Array]") {
curTarget = [];
uniqueMap.set(source, curTarget);
source.forEach(function(child, index, array) {
curTarget[index] = clone(child, uniqueMap);
});
} else if (typeof source == "object") {
// DOM
if (source.nodeType && typeof source.cloneNode == "function") {
curTarget = source.cloneNode(true);
uniqueMap.set(source, curTarget);
} else {
// Date
if (source instanceof Date) {
curTarget = new Date(source);
uniqueMap.set(source, curTarget);
} else {
// it is an object literal
curTarget = {};
uniqueMap.set(source, curTarget);
for (let key in source) {
// 这里一定要用上 Object.prototype.hasOwnProperty.call
if (Object.prototype.hasOwnProperty.call(source, key)) {
curTarget[key] = clone(source[key], uniqueMap);
}
}
}
}
} else {
curTarget = source;
uniqueMap.set(source, curTarget);
}
return curTarget;
}
return clone(source, new Map());
}
参考链接
https://blog.csdn.net/qq_41846861/article/details/102296436
https://stackoverflow.com/questions/4459928/how-to-deep-clone-in-javascript/
并发模型与事件循环
任务与微任务
任务
一个 任务 就是由诸如 从头执行一段程序、执行一个事件回调或一个 interval/timeout 被触发
之类的 标准机制 调度的任意 JavaScript 代码。
这些都安排到了 task queue 上。
之类的 标准机制 调度的任意 JavaScript 代码。
这些都安排到了 task queue 上。
在以下时机,任务会被添加到任务队列:
- 一段新程序或子程序被直接执行时(比如从一个控制台,或在一个 <script> 元素中运行代码)。
- 触发了一个事件,将其回调函数添加到任务队列时。
- 执行到一个由 setTimeout() 或 setInterval() 创建的 timeout 或 interval,以致相应的回调函数被添加到任务队列时。
微任务
一个 微任务(microtask)就是一个简短的函数,
当创建该函数的函数存在,并且只有当 Javascript 调用栈为空,而控制权尚未返还给 event loop 之前
(user agent 用 event loop 来驱动脚本的执行环境),
该微任务才会被执行。
JavaScript 中的 promises 和 Mutation Observer API 都使用微任务队列去运行它们的回调函数。
为了允许第三方库、框架、polyfills 能使用微任务,Window 暴露了 queueMicrotask() 方法,
而 Worker 接口则通过 WindowOrWorkerGlobalScope mixin 。
当创建该函数的函数存在,并且只有当 Javascript 调用栈为空,而控制权尚未返还给 event loop 之前
(user agent 用 event loop 来驱动脚本的执行环境),
该微任务才会被执行。
JavaScript 中的 promises 和 Mutation Observer API 都使用微任务队列去运行它们的回调函数。
为了允许第三方库、框架、polyfills 能使用微任务,Window 暴露了 queueMicrotask() 方法,
而 Worker 接口则通过 WindowOrWorkerGlobalScope mixin 。
任务与微任务的
两个关键区别
两个关键区别
1. 每当一个任务存在,事件循环都会检查该任务是否正把控制权交给其他 JavaScript 代码。
如若不然,事件循环就会运行微任务队列中的所有微任务。
接下来微任务循环会在事件循环的每次迭代中被处理多次,包括处理完事件和其他回调之后。
2. 如果一个微任务通过调用 queueMicrotask(), 向队列中加入了更多的微任务,
则那些新加入的微任务会早于下一个任务运行 。
因为事件循环会持续调用微任务直至微任务队列为空,即使有更多微任务被持续加入。
注意: 因为微任务自身可以入列更多的微任务,且事件循环会持续处理微任务直至队列为空,
那么就存在一种使得事件循环一直处理微任务的真实风险。递归增加微任务是要非常谨慎的。
如若不然,事件循环就会运行微任务队列中的所有微任务。
接下来微任务循环会在事件循环的每次迭代中被处理多次,包括处理完事件和其他回调之后。
2. 如果一个微任务通过调用 queueMicrotask(), 向队列中加入了更多的微任务,
则那些新加入的微任务会早于下一个任务运行 。
因为事件循环会持续调用微任务直至微任务队列为空,即使有更多微任务被持续加入。
注意: 因为微任务自身可以入列更多的微任务,且事件循环会持续处理微任务直至队列为空,
那么就存在一种使得事件循环一直处理微任务的真实风险。递归增加微任务是要非常谨慎的。
参考
https://developer.mozilla.org/zh-CN/docs/Web/API/HTML_DOM_API/Microtask_guide
事件循环
Definitions
To coordinate events, user interaction, scripts, rendering, networking, and so forth,
user agents must use event loops as described in this section.
Each agent has an associated event loop.
user agents must use event loops as described in this section.
Each agent has an associated event loop.
Processing model
An event loop must continually run through the following steps for as long as it exists:
1. Let taskQueue be one of the event loop's task queues
2. Let oldestTask be the first runnable task in taskQueue, and remove it from taskQueue.
3. Set the event loop's currently running task to oldestTask.
4. Let taskStartTime be the current high resolution time.
5. Perform oldestTask's steps.
6. Set the event loop's currently running task back to null.
7. Microtasks: Perform a microtask checkpoint.
8. Let now be the current high resolution time. [HRT]
9. Report the task's duration
10. Update the rendering: if this is a window event loop
1. Let taskQueue be one of the event loop's task queues
2. Let oldestTask be the first runnable task in taskQueue, and remove it from taskQueue.
3. Set the event loop's currently running task to oldestTask.
4. Let taskStartTime be the current high resolution time.
5. Perform oldestTask's steps.
6. Set the event loop's currently running task back to null.
7. Microtasks: Perform a microtask checkpoint.
8. Let now be the current high resolution time. [HRT]
9. Report the task's duration
10. Update the rendering: if this is a window event loop
参考
https://html.spec.whatwg.org/multipage/webappapis.html#event-loops
内存管理
垃圾回收
引用
垃圾回收算法主要依赖于引用的概念。
在内存管理的环境中,一个对象如果有访问另一个对象的权限(隐式或者显式),
叫做一个对象引用另一个对象。
例如,一个Javascript对象具有对它原型的引用(隐式引用)和对它属性的引用(显式引用)。
在这里,“对象”的概念不仅特指 JavaScript 对象,还包括函数作用域(或者全局词法作用域)。
在内存管理的环境中,一个对象如果有访问另一个对象的权限(隐式或者显式),
叫做一个对象引用另一个对象。
例如,一个Javascript对象具有对它原型的引用(隐式引用)和对它属性的引用(显式引用)。
在这里,“对象”的概念不仅特指 JavaScript 对象,还包括函数作用域(或者全局词法作用域)。
GC 领域容易引起歧义的术语
GC 中的"对象"
一个供应用程序使用的数据所对应的内存集合??
组成
头
用于存储和具体数据无关的信息以辅助 GC 算法的实施
域
负责存储具体的数据,一个对象可以有多个 field
GC 中的“根”
GC 领域的所有对象组成了一个树形结构,根便是树的根节点
垃圾收集算法
引用计数算法
(IE6、IE7)
(IE6、IE7)
这是最初级的垃圾收集算法。
此算法把“对象是否不再需要”简化定义为“对象有没有其他对象引用到它”。
如果没有引用指向该对象(零引用),对象将被垃圾回收机制回收。
此算法把“对象是否不再需要”简化定义为“对象有没有其他对象引用到它”。
如果没有引用指向该对象(零引用),对象将被垃圾回收机制回收。
限制:循环引用
该算法有个限制:无法处理循环引用的事例。
在下面的例子中,两个对象被创建,并互相引用,形成了一个循环。
它们被调用之后会离开函数作用域,所以它们已经没有用了,可以被回收了。
然而,引用计数算法考虑到它们互相都有至少一次引用,所以它们不会被回收。
function f(){
var o = {};
var o2 = {};
o.a = o2; // o 引用 o2
o2.a = o; // o2 引用 o
return "azerty";
}
f();
在下面的例子中,两个对象被创建,并互相引用,形成了一个循环。
它们被调用之后会离开函数作用域,所以它们已经没有用了,可以被回收了。
然而,引用计数算法考虑到它们互相都有至少一次引用,所以它们不会被回收。
function f(){
var o = {};
var o2 = {};
o.a = o2; // o 引用 o2
o2.a = o; // o2 引用 o
return "azerty";
}
f();
实例
IE 6, 7 使用引用计数方式对 DOM 对象进行垃圾回收。
该方式常常造成对象被循环引用时内存发生泄漏:
var div;
window.onload = function(){
div = document.getElementById("myDivElement");
div.circularReference = div;
div.lotsOfData = new Array(10000).join("*");
};
在上面的例子里,myDivElement 这个 DOM 元素里的 circularReference 属性引用了 myDivElement,造成了循环引用。
如果该属性没有显示移除或者设为 null,引用计数式垃圾收集器将总是且至少有一个引用,
并将 DOM 元素一直保存在内存中,即使其已经从DOM 树中删去了。
如果这个 DOM 元素拥有大量的数据 (如上的 lotsOfData 属性),这个数据占用的内存将永远不会被释放,
这会导致内存问题,比如浏览器会运行越来越慢。
该方式常常造成对象被循环引用时内存发生泄漏:
var div;
window.onload = function(){
div = document.getElementById("myDivElement");
div.circularReference = div;
div.lotsOfData = new Array(10000).join("*");
};
在上面的例子里,myDivElement 这个 DOM 元素里的 circularReference 属性引用了 myDivElement,造成了循环引用。
如果该属性没有显示移除或者设为 null,引用计数式垃圾收集器将总是且至少有一个引用,
并将 DOM 元素一直保存在内存中,即使其已经从DOM 树中删去了。
如果这个 DOM 元素拥有大量的数据 (如上的 lotsOfData 属性),这个数据占用的内存将永远不会被释放,
这会导致内存问题,比如浏览器会运行越来越慢。
标记-清除算法
(现代浏览器)
(现代浏览器)
这个算法把“对象是否不再需要”简化定义为“对象是否可以获得”。
这个算法假定设置一个叫做根(root)的对象(在Javascript里,根是全局对象)。
垃圾回收器将定期从根开始,找所有从根开始引用的对象,然后找这些对象引用的对象...
从根开始,垃圾回收器将找到所有可以获得的对象和收集所有不能获得的对象。
这个算法假定设置一个叫做根(root)的对象(在Javascript里,根是全局对象)。
垃圾回收器将定期从根开始,找所有从根开始引用的对象,然后找这些对象引用的对象...
从根开始,垃圾回收器将找到所有可以获得的对象和收集所有不能获得的对象。
流程
标记阶段
遍历
深度优先搜索
标记
在搜索到的对象头部添加标记
清除阶段
清除
删除所有未标记的对象。清除已标记对象头部中的标记信息
补充逻辑
合并
未标记对象被清除后,它们对应的内存空间成为空闲状态,
剩余对象可能被分块存储在非连续的内存空间中,这种状况称为碎片化
剩余对象可能被分块存储在非连续的内存空间中,这种状况称为碎片化
碎片化的危害
分块内存的查询效率远低于连续内存
造成存储空间的浪费
合并逻辑会将碎片化的内存重新合并为连续的内存空间
优点
解决了循环引用的问题
在上面的示例中,函数调用返回之后,两个对象从全局对象出发无法获取。
因此,他们将会被垃圾回收器回收。
第二个示例同样,一旦 div 和其事件处理无法从根获取到,
他们将会被垃圾回收器回收。
因此,他们将会被垃圾回收器回收。
第二个示例同样,一旦 div 和其事件处理无法从根获取到,
他们将会被垃圾回收器回收。
足够简单
缺点
需要额外补充逻辑以解决内存碎片化问题
标记阶段的遍历所消耗的时间与对象的数量和规模成正比,
清除阶段的耗时跟堆的容量也成正比,
即标记清除算法的执行效率随着数据量的增长而下降
清除阶段的耗时跟堆的容量也成正比,
即标记清除算法的执行效率随着数据量的增长而下降
浏览器每隔一段时间执行一次 GC 逻辑,这种工作方式会增加浏览器的负荷
从2012年起,所有现代浏览器都使用了标记-清除垃圾回收算法。
所有对JavaScript垃圾回收算法的改进都是基于标记-清除算法的改进,
并没有改进标记-清除算法本身和它对“对象是否不再需要”的简化定义。
所有对JavaScript垃圾回收算法的改进都是基于标记-清除算法的改进,
并没有改进标记-清除算法本身和它对“对象是否不再需要”的简化定义。
内存泄露
指一些分配出去的内存空间在使用完后没有被释放
编写更合理的代码以避免发生内存泄露
避免全局变量
在有些不得不使用全局变量的场景下,比如开发一个工具库或者框架,必须谨慎处理各对象和模块之间的引用关系
谨慎处理闭包
使用闭包时一定要把数据进行合理的作用域划分,
分清哪些适用于放在外层作用域(外层作用域中的数据始终占据着内存空间),
哪些适合放在返回的函数中
分清哪些适用于放在外层作用域(外层作用域中的数据始终占据着内存空间),
哪些适合放在返回的函数中
使用编译工具
代码异味
可能引起深层次问题的“坏”代码
令代码异味“自动”暴露出来
使用代码检测工具
扫描原生JS代码
扫描原生JS代码
ESlint
用编译型语言取代原生JS
TypeScript
JS 中函数是一等公民
一等公民
在编程语言中,
如果一个值可以作为函数参数,可以作为函数返回值,也可以赋值给变量,
那就被称为有“一等公民”的地位
如果一个值可以作为函数参数,可以作为函数返回值,也可以赋值给变量,
那就被称为有“一等公民”的地位
参考
https://www.jianshu.com/p/6b5e02ca8fed
作用域(Scope)
定义
指当前的执行上下文。
在这个上下文中,值与表达式是可见也能够被引用的。
如果一个变量或者其他表达式不在 "当前的作用域",那它不能被使用。
在这个上下文中,值与表达式是可见也能够被引用的。
如果一个变量或者其他表达式不在 "当前的作用域",那它不能被使用。
作用域链
作用域是有层级的,子作用域可以访问父作用域,但父作用域不能访问子作用域。
函数产生的作用域
在 JavaScript 中一个函数将生成一个闭包,因此生成一个作用域。
所以函数内部定义的变量无法从函数外面获取。
所以函数内部定义的变量无法从函数外面获取。
词法/静态作用域
作用域规定了如何查找变量,也就是确定当前执行代码对变量的访问权限。
JavaScript 采用词法作用域(lexical scoping),也就是静态作用域。
静态作用域,函数的作用域在函数定义的时候就决定了。
而与静态作用域相对的是动态作用域,函数的作用域是在函数调用的时候才决定的。
JavaScript 采用的是词法作用域,
但是,它的 eval()、with、this 机制某种程度上很像动态作用域,使用上要特别注意。
JavaScript 采用词法作用域(lexical scoping),也就是静态作用域。
静态作用域,函数的作用域在函数定义的时候就决定了。
而与静态作用域相对的是动态作用域,函数的作用域是在函数调用的时候才决定的。
JavaScript 采用的是词法作用域,
但是,它的 eval()、with、this 机制某种程度上很像动态作用域,使用上要特别注意。
var value = 1;
function foo() {
console.log(value);
}
function bar() {
var value = 2;
foo();
}
bar(); //1
function foo() {
console.log(value);
}
function bar() {
var value = 2;
foo();
}
bar(); //1
执行上下文
(内有变量提升的原因)
(内有变量提升的原因)
什么是执行上下文
In short, the execution context is the abstract concept of the environment in which
JavaScript code is parsed and executed, and any code running in JavaScript runs in the execution context.
执行上下文是 Javascript 代码被解析与执行的环境。所有的 Javascript 代码都在执行上下文中运行。
执行上下文(execution context)简称 EC。
JavaScript code is parsed and executed, and any code running in JavaScript runs in the execution context.
执行上下文是 Javascript 代码被解析与执行的环境。所有的 Javascript 代码都在执行上下文中运行。
执行上下文(execution context)简称 EC。
JS 引擎在运行不同代码时会创建不同的 EC
运行全局级别的代码
浏览器首次运行全局代码时,引擎就会创建一个全局 EC 并将其推入当前的 EC 栈。
运行函数级别的代码
每执行一个函数时,引擎就会创建此函数的 EC 并将其推入 EC 栈栈顶。
运行 Eval 中的代码
在 Eval 函数内运行代码也会产生 EC。
EC 栈
JavaScript is executed on a single thread, and all the code is queued.
When the browser first executes the global code, it first creates the global execution context and pushes it to the top of the execution stack.
Whenever a function is executed, the execution context of the function is created and pushed to the top of the execution stack.
每运行一个函数时,这个函数的 EC 就会被创建并推入 EC 栈顶部。
After the execution of the current function is completed, the execution context of the current function is out of the stack and waiting for garbage collection.
The browser’s JS execution engine always accesses the execution context at the top of the stack.
浏览器的 JS 引擎总是从 EC 栈栈顶取 EC。
There is only one global context, which goes out of the stack when the browser closes.
When the browser first executes the global code, it first creates the global execution context and pushes it to the top of the execution stack.
Whenever a function is executed, the execution context of the function is created and pushed to the top of the execution stack.
每运行一个函数时,这个函数的 EC 就会被创建并推入 EC 栈顶部。
After the execution of the current function is completed, the execution context of the current function is out of the stack and waiting for garbage collection.
The browser’s JS execution engine always accesses the execution context at the top of the stack.
浏览器的 JS 引擎总是从 EC 栈栈顶取 EC。
There is only one global context, which goes out of the stack when the browser closes.
综上意味着每个具体函数执行时,都会创建自己的 EC,并且 JS 引擎执行代码时使用的就是它自己的那个 EC。
示例
var age = 99;
function t() {
console.log(age)
var age = 100
}
t()
function t() {
console.log(age)
var age = 100
}
t()
运行函数 t 时,生成 t 自己的 EC。
此 EC 在创建过程中,该 EC 的 AO 对象中 age 属性为 undefined。
JS 引擎使用该 EC 执行函数体代码,第一句输出 undefined ,第二句给该 EC 的 AO 的 age 属性赋值。
此 EC 在创建过程中,该 EC 的 AO 对象中 age 属性为 undefined。
JS 引擎使用该 EC 执行函数体代码,第一句输出 undefined ,第二句给该 EC 的 AO 的 age 属性赋值。
EC 所包括的内容
VO:{/* 上下文中的数据
( 函数形参,函数声明, 变量声明)
*/}
( 函数形参,函数声明, 变量声明)
*/}
VO 是一种让 EC 知道数据存储在哪以及如何获取它们的机制。
包含内容
函数形参
函数声明
变量声明
ES5 中的 VO
In ES5 the concept of variable object is replaced with lexical environments model
不同执行上下文
中的 VO
中的 VO
GlobalContext
VO === this === global
FunctionContext
VO === AO,
<arguments> object and <formal parameters> are added
<arguments> object and <formal parameters> are added
AO
在函数的执行上下文中,VO是不能直接访问的。它主要扮演被称作活跃对象(activation object的角色。
也就是当EC环境为函数时,我们访问的是AO,而不是VO。
也就是当EC环境为函数时,我们访问的是AO,而不是VO。
AO是在进入函数的执行上下文时创建的,并为该对象初始化一个arguments属性,该属性的值为Arguments对象。
AO = {
arguments: {
callee:,
length:,
properties-indexes: //函数传参参数值
}
};
AO = {
arguments: {
callee:,
length:,
properties-indexes: //函数传参参数值
}
};
Scope:{ /* VO以及所有父执行上下文中的VO */}
this:{}
EC 的阶段
创建阶段
创建 VO
(变量提升的原因)
(变量提升的原因)
检查函数的形参,在变量对象创建属性,其属性名就是形参的名字,其值就是实参的值;
对于没有传递的参数,其值为undefined;
对于没有传递的参数,其值为undefined;
检查函数声明,在变量对象上创建属性,其属性名就是函数名,值指向内存中的函数;
如果变量对象已经包含了相同名字的属性,则替换它的值;
如果变量对象已经包含了相同名字的属性,则替换它的值;
检查变量声明,在变量对象上创建属性,其属性名即为变量名,初始化其值为 undefined;
如果变量名和已经声明的函数名或者函数的参数名相同,则不会影响已经存在的属性。
如果变量名和已经声明的函数名或者函数的参数名相同,则不会影响已经存在的属性。
function add(num) {
var num;
console.log(num); //1
}
add(1);
var num;
console.log(num); //1
}
add(1);
创建 Scope Chain
在 VO 之后创建
确定 this 指向
执行阶段
JS 引擎使用此 EC 逐行执行代码,并在此过程中给 VO 变量属性 赋值
回收阶段
执行上下文出栈等待虚拟机回收执行上下文
参考链接
http://www.itboth.com/d/UbIZJ3/javascript-ao-vo
http://dmitrysoshnikov.com/ecmascript/chapter-2-variable-object
https://developpaper.com/deep-understanding-of-javascript-execution-context-and-execution-stack/
https://www.cnblogs.com/congxueda/p/10806927.html
闭包(closure)
闭包
定义
函数与对其状态即词法环境(lexical environment)的引用共同构成闭包(closure)
作用
可以让你从内部函数访问外部函数作用域
生成时间
在JavaScript,函数在每次创建时生成闭包。
组成
函数
声明该函数的词法环境
这个环境包含了这个闭包创建时所能访问的所有局部变量
用途
模块模式
使用闭包来定义公共函数,并令其可以访问私有函数和变量
var makeCounter = function() {
var privateCounter = 0;
function changeBy(val) {
privateCounter += val;
}
return {
increment: function() {
changeBy(1);
},
decrement: function() {
changeBy(-1);
},
value: function() {
return privateCounter;
}
}
};
var Counter1 = makeCounter();
var Counter2 = makeCounter();
console.log(Counter1.value()); /* logs 0 */
Counter1.increment();
Counter1.increment();
console.log(Counter1.value()); /* logs 2 */
Counter1.decrement();
console.log(Counter1.value()); /* logs 1 */
console.log(Counter2.value()); /* logs 0 */
var privateCounter = 0;
function changeBy(val) {
privateCounter += val;
}
return {
increment: function() {
changeBy(1);
},
decrement: function() {
changeBy(-1);
},
value: function() {
return privateCounter;
}
}
};
var Counter1 = makeCounter();
var Counter2 = makeCounter();
console.log(Counter1.value()); /* logs 0 */
Counter1.increment();
Counter1.increment();
console.log(Counter1.value()); /* logs 2 */
Counter1.decrement();
console.log(Counter1.value()); /* logs 1 */
console.log(Counter2.value()); /* logs 0 */
性能
如果不是某些特定任务需要使用闭包,在其它函数中创建函数是不明智的,
因为闭包在处理速度和内存消耗方面对脚本性能具有负面影响。
因为闭包在处理速度和内存消耗方面对脚本性能具有负面影响。
例子
在创建新的对象或者类时,方法通常应该定义在对象的原型上,而不是定义到对象的构造器中。
将方法定义到构造器中时,每次对象创建时构造器被调用,方法都会被重新赋值一次。
将方法定义到构造器中时,每次对象创建时构造器被调用,方法都会被重新赋值一次。
function MyObject(name, message) {
this.name = name.toString();
this.message = message.toString();
this.getName = function() {
return this.name;
};
this.getMessage = function() {
return this.message;
};
}
this.name = name.toString();
this.message = message.toString();
this.getName = function() {
return this.name;
};
this.getMessage = function() {
return this.message;
};
}
function MyObject(name, message) {
this.name = name.toString();
this.message = message.toString();
}
// 不建议重新定义原型,要将新的方法追加到原型上
MyObject.prototype.getName = function() {
return this.name;
};
MyObject.prototype.getMessage = function() {
return this.message;
};
this.name = name.toString();
this.message = message.toString();
}
// 不建议重新定义原型,要将新的方法追加到原型上
MyObject.prototype.getName = function() {
return this.name;
};
MyObject.prototype.getMessage = function() {
return this.message;
};
this
显性绑定
隐性绑定
默认绑定
new
new 运算符创建一个用户定义的对象类型的实例或具有构造函数的内置对象的实例。
当代码 new Foo(...) 执行时,会发生以下事情:
- 一个继承自 Foo.prototype 的新对象被创建。
- 使用指定的参数调用构造函数 Foo,并将 this 绑定到新创建的对象。当没有指定参数列表时,new Foo 等同于 new Foo()。
- 由构造函数返回的对象就是 new 表达式的结果。如果构造函数没有显式返回一个对象,则使用步骤1创建的对象。
(一般情况下,构造函数不返回值,但是用户可以选择主动返回对象,来覆盖正常的对象创建步骤)
JS 引擎
A JavaScript engine is a computer program that executes JavaScript (JS) code.
The first JavaScript engines were mere interpreters,
but all relevant modern engines utilize just-in-time compilation for improved performance.
JavaScript engines are typically developed by web browser vendors, and every major browser has one.
In a browser, the JavaScript engine runs in concert with the rendering engine via the Document Object Model.
The use of JavaScript engines is not limited to browsers.
For example, the Chrome V8 engine is a core component of the popular Node.js runtime system.
The first JavaScript engines were mere interpreters,
but all relevant modern engines utilize just-in-time compilation for improved performance.
JavaScript engines are typically developed by web browser vendors, and every major browser has one.
In a browser, the JavaScript engine runs in concert with the rendering engine via the Document Object Model.
The use of JavaScript engines is not limited to browsers.
For example, the Chrome V8 engine is a core component of the popular Node.js runtime system.
即时编译
Just-in-time compilation
Just-in-time compilation
In computing, just-in-time (JIT) compilation (also dynamic translation or run-time compilations)
is a way of executing computer code that involves compilation during execution of a program
– at run time – rather than before execution.
理解成在运行时编译代码,而不是在运行前编译。
is a way of executing computer code that involves compilation during execution of a program
– at run time – rather than before execution.
理解成在运行时编译代码,而不是在运行前编译。
Runtime (program lifecycle phase)
In computer science, runtime, run time or execution time is the time when the CPU is executing the machine code.
It is the last stage of a program's lifecycle.
It is the last stage of a program's lifecycle.
参考
https://en.m.wikipedia.org/wiki/JavaScript_engine
0 条评论
下一页
为你推荐
查看更多