学习方式:一边看书,一边学习他人的博客,把一些关键的部分记录在此,其他的贴出他人博客的链接。此外,本文并非仅是《你不知道的JS》笔记,还额外补充一些内容。
文章目录
最佳实践/原则
- 最小特权原则:在软件设计中,应该最小限度地暴露必要内容,而将其他内容都“隐藏”起来,比如某个模块或对象的API设计
- 始终给函数表达式命名
- 对不再必要的全局变量或全局对象的属性,将其设置为null
上卷
作用域
1.Javascript引擎,编译器,作用域三者之间的关系及LHS和RHS的区别
2.聊聊JavaScript 编译器,引擎,作用域
JS的编译
JS代码片在执行前要先编译:它的编译过程(通常)是在实际执行前进行的,而且也不会产生可移植的编译结果。
通常的编译步骤:
分词与词法分析:把输入的字符串分解为一些对编程语言有意义的代码块(词法单元)。解析与语法分析:将上一步的词法单元集合分析并最终转换为一个由元素逐级嵌套所组成的代表了程序语法结构的树,称为抽象语法树(Abstract Syntax Tree,AST)。代码生成:将上一步的AST转换为可执行代码。
由于JS编译的特殊性,编译执行效率就要比一般静态语言敏感的多,故而也非常复杂。JS引擎在这一部分做了非常多的优化,一是针对语法分析和代码生成阶段进行优化(例如针对冗余元素进行优化等),目的是提高编译后的执行效率。二是针对编译过程进行优化(如JIT,延迟编译甚至重编译),目的是缩短编译过程,保证性能最佳。
引擎、编译器和作用域
引擎: 负责整个Javascript程序的编译及执行过程。
编译器:负责语法分析及代码生成。
作用域:负责收集并维护有所有声明的标识符组成的一系列查询。
var a=1的编译过程
编译器首先会将这段代码分解成词法单元,然后将词法单元解析成树结构。- 对词法单元进行解析,解析到var a时,
编译器会询问作用域是否存在一个变量名为a在同一作用域的集合中。如果有,编译器就忽略此声明。反之,在要求的作用域下声明变量。
第三步:生成可以运行代码(=1)给引擎执行,生成代码的这个过程就涉及到LHS和RHS两种赋值概念。
第四步:引擎运行编译器生成的代码时,会询问作用域是否存在在当前作用域下变量名为a的集合,如果没有,则在向上一级作用域查找变量名a。如果有,引擎则对变量名为a的集合赋值。
LHS和RHS
LHS(left-hand-side):找到变量的容器本身,从而可以对其赋值
RHS(rigjt-hand-side):查找某个变量的值
函数作用域和块作用域
最小特权原则
最小特权原则:在软件设计中,应该最小限度地暴露必要内容,而将其他内容都“隐藏”起来,比如某个模块或对象的API设计
规避冲突
全局命名空间
(可以实践)
在全局作用域中声明一个名字足够独特的变量,通常是一个对象。这个对象被用作库的命名空间,所有需要暴露给外界的功能都会称为这个对象的属性,而不是将自己的标识符暴露在顶级的词法作用域中。
IIFE
把IIFE当作函数调用并传递参数进去,可以改进代码风格
var a = 2;
(function IIFE(global) {
var a = 2;
console.log(a); //3
console.log(global.a); //2
})(window);
console.log(a); //2
好处:在代码风格上对全局对象的引用,变得比引用一个没有"全局"字样的变量更加清晰
倒置代码的运行顺序,将需要运行的函数放在第二位,在IIFE执行之后当作参数传递进去
(思考:这种方式或许能在某处派上用场)
var a = 2;
(function IIFE(def) {
def(window);
})(function def(global){
var a = 3;
console.log(a); //3
console.log(global.a); //2
})
with和try/catch(放着)
垃圾回收(补充)
前端面试:谈谈 JS 垃圾回收机制
浏览器垃圾回收与内存管理
引用计数法由于循环引用问题而遭到废弃,此处只记录标记清理法:
- 垃圾回收程序运行的时候,会标记内存中存储的所有变量
- 程序会将所有在上下文中的变量,以及被在上下文中的变量引用的变量的标记去掉,在此之后再被加上标记的变量就是待删除的,因为任何在上下文中的变量都访问不到它们了
- 随后垃圾回收程序做一次内存清理,销毁带标记的所有值并回收它们的内存
关于垃圾回收更详细的内容,请猛戳第二篇博文
V8垃圾回收
V8三种回收算法的比较:
| 回收算法 | 标记清除(Mark-Sweep) | 标记整理 | Scavange |
|---|---|---|---|
| 速度 | 中等 | 最慢 | 最快 |
| 空间开销 | 少(有碎片) | 少(无碎片) | 双倍空间(无碎片) |
| 是否移动对象 | 否 | 是 | 是 |
模块(放着)
动态作用域
- 词法作用域:在写代码或者说定义时确定,关注函数在何处声明
- 动态作用域:在运行时确定,关注函数在何处调用,作用域基于调用栈
this
往日结论
先说以前记的结论:
1.以函数形式调用时,this永远都是window
2.以方法的形式调用时,this是调用方法的对象
3.以构造函数的形式调用时,this是新创建的那个对象
4.使用call和apply调用时,this是指定的那个对象
关于this
- this在运行时进行绑定,它的上下文取决于函数调用时的各种条件。
- 当一个函数被调用时,会创建一个活动记录(执行上下文),它包含函数在哪里被调用(调用栈)、函数的调用方式、传入的参数等信息。this是执行上下文的一个属性,会在函数执行的过程中用到
this全面解析
显示强制绑定:一旦绑定this后不可以再通过call或apply修改this,比如ES添加的bind
优先级
- new(构造调用)
- 显示绑定/硬绑定调用(call、apply/bind)
- 隐式绑定(对象的方法)
- 默认绑定(全局对象)
对默认绑定来说,决定this绑定对象的并不是调用位置是否处于严格模式,而是函数体是否处于严格模式。如果函数体处于严格模式,this会被绑定到undefined,否则this会被绑定到全局对象。
特殊情况
将null或undefined作为绑定对象
这些情况下,函数并不关心this是什么,传入的null仅仅作为占位值
情况一:展开数组;情况二:柯里化(减少返回的函数要求传入参数的个数)
function foo(a, b) {
console.log("a:" + a + ", b:" + b);
}
//把数组展开
foo.apply(null, [2,3]); // a: 2, b: 3
//ES6中可以用...操作符来替代
//使用bind进行柯里化
let bar = foo.bind(null, 2);//对bar,以后就只用传入b这个参数了
bar(3); // a: 2, b: 3
以上的方式有一定安全隐患,并不推荐
更安全的this
创建空的非委托对象,即DMZ(非军事区)对象
Object.create(null)和{}很像,但是并不会创建Object.prototype这个委托,因此比{}更空。
function foo(a, b) {
console.log("a:" + a + ", b" + b);
}
//我们的DMZ空对象,使用空集符号可以增强可读性
let Ø = Object.create(null);
//把数组展开
foo.apply(Ø, [2,3]); // a: 2, b: 3
//使用bind进行柯里化
let bar = foo.bind(Ø, 2);//对bar,以后就只用传入b这个参数了
bar(3); // a: 2, b: 3
间接引用
创建函数的间接引用时,调用该函数会应用默认绑定规则,间接引用最容易在赋值时发生
function foo() {
console.log(this.a);
}
let a = 2;
let o = {a: 3, foo: foo};
let p = {a: 4};
o.foo(); // 3
(p.foo = o.foo)(); // 2
//赋值表达式p.foo = o.foo 的返回值是目标函数的引用,因此调用位置是foo()而不是p.foo()或o.foo()
软绑定
由以上可知,硬绑定后隐式绑定或显示绑定会无效
软绑定:可以给默认绑定指定一个全局对象和undefined以外的值,同时保留隐式绑定或者显式绑定修改this的能力
// 这一段代码,暂时不能完全看懂
if(!Function.prototype.softBind) {
Function.prototype.softBind = function(obj) {
let fn = this;
// 捕获所有curried参数
let curried = [].slice.call(arguments,1);
let bound = function() {
return fn.apply(
(!this || this === (window || global)) ?
obj: this,
curried.concat.apply(curried, arguments)
);
};
bound.prototype = Object.create(fn.prototype);
return bound;
};
}
function foo() {
console.log("name: " + this.name);
}
let obj = {name: "obj"}, obj2 = {name: "obj2"}, obj3 = {name: "obj3"};
let fooOBJ = foo.softBind(obj); //软绑定,此时默认绑定了obj对象
fooOBJ(); // name: obj
obj2.foo = foo.softBind(obj); //软绑定,此时默认绑定了obj对象
obj2.foo(); // name: obj2 此时隐式绑定仍然生效
fooOBJ.call(obj3); // name: obj3 此时显示绑定仍然生效
setTimeout(obj2.foo, 10); // name: obj
箭头函数
箭头函数不使用this的四种标准规则,而是根据外层作用域来决定this
箭头函数的绑定无法被修改,new也不行
function foo() {
return (a) => {
console.log(this.a);
}
}
let obj1 = {a: 2};
let obj2 = {a: 3};
let bar = foo.call(obj1);
//foo的this被绑定为obj1,由this词法,bar的this受到foo的影响,也是obj1
bar.call(obj2); // 2, 一旦绑定,就不可被修改
function foo() {
setTimeout(() => {
})
}