就是对一些js比较高级一点的知识的简单提及
各种杂乱知识点
1.变量提升
var a = 10;
{
function hello() {
a = 99;
function a() {
}
a = 30;
}
hello();
}
console.log(a); //非严格模式下,输出99,严格模式下就不会出现这种奇怪的错误
var是函数作用域(包含它的函数的局部变量),let是块级作用域(类似一个大括号里面的作用域包括if,while,function等)。
使用var时,会自动将声明提升到函数的顶部,如果在全局里面声明的,就提升到全局的顶部,而且只是对声明进行提升,实际上的赋值还是在原来的地方,所以可以看见var的多次声明一个变量并赋值,其实只是声明了一次,后面都是赋值。但let就不会进行提升,所以不能重复声明同一变量,也不能在定义之前进行使用。
对于函数定义,还有函数声明提升,类似于var。但函数表达式就不可以了
//函数声明
consolo.log(sum(10))
function sum(num){return num}
//函数表达式,在之前调用会error,而且不是let的原因
let sum = function(num){return num};
//除此之外,这两种方式等价
2.堆栈,深拷贝,浅拷贝
堆其实是一种特殊的树。只要满足这两点,它就是一个堆。
- 堆是一个完全二叉树。 完全二叉树:除了最后一层,其他层的节点个数都是满的,最后一层的节点都靠左排列。
- 堆中每一个节点的值都必须大于等于(或小于等于)其子树中每个节点的值。 也可以说:堆中每个节点的值都大于等于(或者小于等于)其左右子节点的值。这两种表述是等价的。
对于数据存储:
- 基本类型是保存在栈内存中的简单数据段,它们的值都有固定的大小,保存在栈空间,通过按值访问,并由系统自动分配和自动释放。 这样带来的好处就是,内存可以及时得到回收,相对于堆来说,更加容易管理内存空间。 JavaScript 中的
Boolean、Null、Undefined、Number、String、Symbol
都是基本类型。 - 引用类型(如对象、数组、函数等)是保存在堆内存中的对象,值大小不固定,栈内存中存放的该对象的访问地址指向堆内存中的对象,JavaScript 不允许直接访问堆内存中的位置,因此操作对象时,实际操作对象的引用。 JavaScript 中的
Object、Array、Function、RegExp、Date
是引用类型。
深拷贝:复制得到的访问地址指向不同的内存空间,互不相干
。所以修改其中一个值,另外一个不会改变。
平时使用数组复制时,我们大多数会使用 =
,这只是浅拷贝,存在很多问题,会相互影响
let arr = [1,2,3,4,5];
let arr2 = arr;//或arr2 = Object.assign(arr)
console.log(arr) //[1, 2, 3, 4, 5]
console.log(arr2) //[1, 2, 3, 4, 5]
arr[0] = 6;
console.log(arr) //[6, 2, 3, 4, 5]
console.log(arr2) //[6, 2, 3, 4, 5]
arr2[4] = 7;
console.log(arr) //[6, 2, 3, 4, 7]
console.log(arr2) //[6, 2, 3, 4, 7]
深拷贝的的复制过程
let a = { x: 10, y: 20 }
//先转换成字符类型,再转成object类型,成功在堆里面生成了一个新的对象数据,但是在这是一个function对象的时候就会发生错误
let b = JSON.parse(JSON.stringify(a));
b.x = 5;
console.log(a.x); // 10
console.log(b.x); // 5
在原文章中还有一些对数组这些的一些方法(如slice,concat)的测试,实现深拷贝
书籍
js高级程序设计
p236 实例只有指向原型的指针,没有指向构造函数的指针
p237 原型的问题还是源自其自身的共享特性,当原型上面存在引用类型(如数组)的时候,这个引用类型是共享的。当一个实例修改了这个数组,其他实例也能得到这个数组,一般而言,每个实例应该有自身的属性副本。
可使用盗用构造函数(在子类构造函数中使用call()或apply()调用父类构造函数)
和组合继承(主流)(结合原型链和盗用构造函数,两者都使用)解决
p240 默认情况下,所有引用类型都继承自Object
p284 代理进行对赋值操作的属性检验
p289 因为函数名是指向函数的指针,所以一个函数可以有多个名称
p300 function定义的函数里面的this引用的是这个函数被调用时所处的作用域,而箭头函数就只是被定义时的上下文
p352 await关键字只能直接出现在异步函数的定义中
p467 css属性名使用连字符表示法,所以就算使用的时候必须将其转为驼峰命名
你所不知道的js–上
解析/语法分析(Parsing)
这个过程是将词法单元流(数组)转换成一个由元素逐级嵌套所组成的代表了程序语法 结构的树。这个树被称为“抽象语法树”(Abstract Syntax Tree,AST)。
eval(…) 函数可以接受一个字符串为参数,并将其中的内容视为好像在书 写时就存在于程序中这个位置的代码。换句话说,可以在你写的代码中用程序生成代码并 运行,就好像代码是写在那个位置的一样
要避免书写eval和with这两个语法,因为会影响js引擎的编译性能
函数作用域的含义是指,属于这个函数的全部变量都可以在整个函数的范围内使用及复 用(事实上在嵌套的作用域中也可以使用)。
闭包
本质上无论何时何地,如果将函数(访问它们各自的词法作用域)当作第一级的值类型并到处传递,你就会看到闭包在这些函数中的应用。在定时器、事件监听器、 Ajax 请求、跨窗口通信、Web Workers 或者任何其他的异步(或者同步)任务中,只要使 用了回调函数,实际上就是在使用闭包!
闭包有3个特性:
①函数嵌套函数
②函数内部可以引用函数外部的参数和变量
③参数和变量不会被垃圾回收机制回收
P48 在循环里面的settimeout函数里面定义的函数的作用域解释
//其实直接用let就行,但需要明白后面的原理
for (var i=1; i<=5; i++) {
setTimeout( function timer() {
console.log( i );
}, i*1000 );
}
//结果是:会以每秒一次的频率输出五次 6。这就是作用域的使用不当,js是单线程的,所以在执行for循环的时候定时器setTimeout被安排到任务队列中排队等待执行,而在等待过程中for循环就已经在执行,等到setTimeout可以执行的时候,for循环已经结束,i的值也已经编程5
//改进
for (var i=1; i<=5; i++) {
(function(j) {
setTimeout( function timer() { console.log( j ); }, j*1000 );
})( i );
}
for 循环头部的 let 声明还会有一 个特殊的行为。这个行为指出变量在循环过程中不止被声明一次,每次迭代都会声明。随 后的每个迭代都会使用上一个迭代结束时的值来初始化这个变量。
简单来说闭包实则是词法作用域的产物;一个函数只要是在内部函数访问到外部函数的作用域我们则可以称之为闭包
//这也是一个闭包
function foo(){
var a = 'out var';
function bar(){
console.log(a);
}
bar();
}
foo();
对于闭包,更常见的方式应该是模块模式,模块里面运行定义私有的实现(函数,变量 ),同时也可以暴露一些方法作为公开的API
this和原型
this 是在运行时进行绑定的,并不是在编写时绑定,它的上下文取决于函数调 用时的各种条件。this 的绑定和函数声明的位置没有任何关系,只取决于函数的调用方式
函数的调用栈,绑定规则
就可以顺序应用下面这四条规则来判断 this 的绑定对象。
-
由 new 调用?绑定到新创建的对象。
-
由 call 或者 apply(或者 bind)调用?绑定到指定的对象。
-
由上下文对象调用?绑定到那个上下文对象。
-
默认:在严格模式下绑定到 undefined,否则绑定到全局对象
P115 [[PUT]] 和 [[GET]]是对对象的描述表示,而setter和getter是对对象单个属性值的表示
setter 会覆盖单个属性默认的 [[Put]](也被称为赋值)操作
in 操作符可以检查容器内是否有某个值,但是它实际上检查的是某 个属性名是否存在。对于数组来说这个区别非常重要,
4 in [2, 4, 6]
的结 果并不是你期待的 True,因为 [2, 4, 6] 这个数组中包含的属性名是 0、1、 2,没有 4。
Object.keys(…) 会返回一个数组,包含所有可枚举属性,Object.getOwnPropertyNames(…) 会返回一个数组,包含所有属性,无论它们是否可枚举。
in 和 hasOwnProperty(…) 的区别在于是否查找 [[Prototype]] 链(in会查找原型链),然而,Object.keys(…) 和 Object.getOwnPropertyNames(…) 都只会找对象直接包含的属性
你所不知道的js–中
类型与语法
JavaScript 中的变量是没有类型的,只有值才有。变量可以随时持有任何类型的值
变量在未持有值的时候为 undefined。此时 typeof 返回 “undefined”:
数组通过数字进行索引,但有趣的是它们也是对象,所以也可以包含字符串键值和属性(但这些并不计算在数组长度内)
a=[]
a[0] = 1
a['bc'] = 2
a.lenght // 1
a.bc // 2
//注意这个点的强制转换
a['23'] = 23
a.length // 24
• undefined 指从未赋值
• null 指曾赋过值,但是目前没有值
null 是一个特殊关键字,不是标识符,我们不能将其当作变量来使用和赋值。然而
undefined 却是一个标识符,可以被当作变量来使用和赋值。
由于 NaN 和自身不相等,所以必须使用 ES6 中的 Number.isNaN(…)
ES6 中新加入了一个工具方法 Object.is(…) 来判断两个值是否绝对相等,主要用于特殊的判断,如NAN和+0,-0,Infinity,-Infinity
以下这些是假值:
• undefined
• null
• false
• +0、-0 和 NaN
• “”
假值的布尔强制类型转换结果为 false。
从逻辑上说,假值列表以外的都应该是真值(truthy)。但 JavaScript 规范对此并没有明确定义,只是给出了一些示例,例如规定所有的对象都是真值,我们可以理解为假值列表以外的值都是真值。
例如:[], {}, function(){}都是真值
我们要对 == 两边的值认真推敲,以下两个原则可以让我们有效地避免出错。
• 如果两边的值中有 true 或者 false,千万不要使用 ==。
• 如果两边的值中有 []、"" 或者 0,尽量不要使用 ==。这时最好用 === 来避免不经意的强制类型转换。这两个原则可以让我们避开几乎所有强制类型转换的坑。
所以 == 和 === 选择哪一个取决于是否允许在相等比较中发生强制类型转换
语句都有一个结果值(statement completion value,undefined 也算),获得结果值最直接的方法是在浏览器开发控制台中输入语句,默认情况下控制台会显示所执行的最后一条语句的结果值。
如果在控制台中输入 var a = 42 会得到结果值 undefined,而非 42
换句话说,代码块的结果值就如同一个隐式的返回,即返回最后一个语句的结果值
delete 用来删除对象中的属性和数组中的单元
JavaScript 中的 && 和 || 运算符返回它们其中一个操作数的值,而非true 或 false
-
42 || ’b‘; // 42
-
true || false && false; // true
-
(true || false) && false; // false
-
true || (false && false); // true
这说明 && 运算符先于 || 执行,而且执行顺序并非我们所设想的从左到右。原因就在于运算符优先级
暂时性死区:指的是由于代码中的变量还没有初始化而不能被引用的情况,对此,最直观的例子是 ES6 规范中的 let 块作用域,另一个 TDZ 违规的例子是 ES6 中的参数默认值{
a = 2; // ReferenceError!
let a;
}
异步函数的回调地狱
//假设所有的函数都是异步回调函数,最后的运行顺序会是ABCDEF
doA( function(){
doC();
doD( function(){
doF();
} )
doE();
} );
doB();
通过回调表达程序异步和管理并发的两个主要缺陷:缺乏顺序性和可信任性(控制反转)。
PROMISE
虽然之前也写过一些关于promise的文字,但现在还是为了加深记忆,再写一些吧
注意异步任务队列这个东西,会决定异步代码和同步代码的运行顺序
链式流
如果向 Promise.resolve(…) 传递一个非 Promise、非 thenable 的立即值,就会得到一个用这个值填充的 promise
而如果向 Promise.resolve(…) 传递一个真正的 Promise,就只会返回同一个 promise.对传入的 thenable 则会展开。如果这个 thenable 展开得到一个拒绝状态,那么从 Promise.resolve(…) 返回的 Promise 实际上就是这同一个拒绝状态
reject(…) 不会像 resolve(…) 一 样 进 行 展 开。 如 果 向reject(…) 传入一个 Promise/thenable 值,它会把这个值原封不动地设置为
拒绝理由。后续的拒绝处理函数接收到的是你实际传给 reject(…) 的那个Promise/thenable,而不是其底层的立即值
Promise 链的一个最佳实践就是最后总以一个 catch(…) 结束
catch也会返回一个promise实例,并且是resolved状态。throw new Error()
抛出错误变为rejected状态
var p = Promise.resolve( 21 );
p.then( function(v){
console.log( v ); // 21
// 创建一个promise并将其返回
return new Promise( function(resolve,reject){
// 用值42填充
resolve( v * 2 );
} );
} )
.then( function(v){
console.log( v ); // 42
} );
虽然我们把 42 封装到了返回的 promise 中,但它仍然会被展开并最终成为链接的 promise的决议,因此第二个 then(…) 得到的仍然是 42。如果我们向封装的 promise 引入异步,一切都仍然会同样工作
Promise.all([promise1,promise2… ])
同时执行两个或更多步骤(也就是“并行执行”)。它们的完成顺序并不重要,但是必须都要完成,才能继续进行其他步骤
Promise.all([ … ]) 需要一个参数,是一个数组,通常由 Promise 实例组成,当然就算不是promise也行,因为其实里面的都会被resolve一次,确保得到一有个真正的promise。如果数组是空的,主 Promise 就会立即完成。
从 Promise.all([ … ]) 调用返回的 promise 会收到一个完成消息。这是一个由所有传入 promise 的完成消息组成的数组,与指定的顺序一致(与完成顺序无关)
Promise.all([ … ]) 返回的主 promise 在且仅在所有的成员 promise 都完成后才会完成。如果这些 promise 中有任何一个被拒绝的话,主 Promise.all([ … ])promise 就会立即被拒绝,并丢弃来自其他所有 promise 的全部结果。
Promise.race([promise1,promise2…])
与 Promise.all([ … ]) 类似,一旦有任何一个 Promise 决议为完成,Promise.race([ … ])就会完成;一旦有任何一个 Promise 决议为拒绝,它就会拒绝。但不能传递空数组,不然会被一直挂住,不会返回
web worker
在js中运行一个类似多线程的程序
var w1 = new Worker( "http://some.url.1/mycoolworker.js" );
注意是一个js文件,Worker 之间以及它们和主程序之间,不会共享任何作用域或资源,不会发生抢占,锁这些多线程的东西
Worker w1 对象是一个事件侦听者和触发者,可以通过订阅它来获得这个 Worker 发出的事件以及发送事件给这个 Worker
//主线程侦听事件
w1.addEventListener( "message", function(evt){
// evt.data
} );
//发送 "message" 事件给这个 Worker:
w1.postMessage( "something cool to say" );
//在这个 Worker 内部,收发消息是完全对称的:
// "mycoolworker.js"
addEventListener( "message", function(evt){
// evt.data
} );
postMessage( "a really cool reply" );
要在创建 Worker 的程序中终止 Worker,可以调用 Worker 对象(就像前面代码中的 w1)上的 terminate()。突然终止 Worker 线程不会给它任何机会完成它的工作或者清理任何资源。这就类似于通过关闭浏览器标签页来关闭页面
Web Worker 通常应用于哪些方面呢?
- 处理密集型数学计算
- 大数据集排序
- 数据处理(压缩、音频分析、图像处理等)
- 高流量网络通信
具体的实现和操作,到时候看文档吧