-
对于传统编译型语言来说,编译步骤分为:
词法分析
、语法分析
、语义检查
、代码优化
和字节生成
。对于 js 解释型语言来说,通过词法分析
和语法分析
得到语法树
后,就可以开始解释执行了。 -
js 是词法作用域(静态作用域),和动态作用域的区别是:
- 词法作用域是指在
书写代码
时或者说定义时确定了作用域,关注函数在何处声明
。词法作用域中的函数遇到既不是形参又不是函数内部定义的局部变量时,会去函数定义
时的环境查询。 - 而动态作用域是在
运行时
确定作用域,关注函数从何处调用
。动态作用域中的函数遇到既不是形参又不是函数内部定义的局部变量时,到函数调用
时的环境查询。
// 词法作用域 lexical scope function foo() { console.log(a); //输出1 } function bar() { var a = 2; foo(); } var a = 1; bar(); // 动态作用域 dynamic scope function foo() { console.log(a); //输出2 } function bar() { var a = 2; foo(); } var a = 1; bar();
- 词法作用域是指在
-
但是 js 可以使用
with eval
构建动态作用域。 -
大多语言都是静态作用域和块结构,js 补充了 let 也提供了块结构。静态作用域的语言,基本都是采用
最内嵌作用域规则
,并且控制栈的活动记录中通过访问链
(access link)维护静态作用域。 -
js 引擎在执行每个函数实例时,都会创建一个
执行环境
(execution context)。执行环境中包含一个调用对象
(call object), 调用对象是一个脚本对象
(scriptObject)结构,用来保存内部变量表
(varDecls)、内嵌函数表
(funDecls)、父级引用列表
(upvalue)等语法分析结构。内部变量表
和内嵌函数表
等信息是在语法分析阶段就已经得到,并保存在语法树中。函数实例执行时,会将这些信息从语法树复制到脚本对象
上。脚本对象
是与函数相关的一套静态系统,与函数实例的生命周期保持一致。 -
词法作用域
(lexcical scope)是 js 的作用域机制,还需要理解它的实现方法,这就是作用域链
(scope chain)。作用域链是一个命名查询
(name lookup)机制,首先在当前执行环境的 scriptObject 中寻找,没找到则顺着 upvalue 到父级 scriptObject 中寻找,一直 lookup 到全局调用对象
(global object)。 -
当一个函数实例执行时,会创建或关联到一个
闭包
(closure)。脚本对象
用来静态保存与函数相关的变量表,闭包
则在执行期动态保存这些变量表及其运行值。闭包
的生命周期有可能比函数实例长。函数实例在活动引用为空后会自动销毁,闭包
则要等要数据引用为空后,由 js 引擎回收(有些情况下不会自动回收,就导致了内存泄漏)。 -
函数内名称解析顺序:比如,当访问函数内的 foo 变量时,JavaScript 会按照下面顺序查找:
- 当前作用域内是否有 var foo 的定义;
- 函数形式参数是否有使用 foo 名称的;
- 函数自身是否叫做 foo;
- 回溯到上一级作用域,然后从 #1 重新开始。
-
js 解释器执行代码前,先创建全局对象,进行预解析。
-
js 引擎会优先解析
var 变量
和function 定义
。- 对于
var
定义的变量,无论该变量自身有没有赋值,在预解析阶段都先赋值undefined
。意思是var
定义的变量提升,只是预定义变量却不赋值,即该变量值为undefined
。 - 对于
function
定义的函数,进行预解析的时候,不仅是声明了函数而且还定义了函数。但是它存储数据的空间里面,存储的是代码字符串,没有任何意义。后面同名定义的函数会覆盖前面定义的。也不会受到 function 里面的 return 影响。
- 对于
-
预解析是不受
if
或者其它判断条件影响的。也就是说,即使条件不成立,我们里面只要有 var 或者 function 也会被预解析。 -
预解析是分段进行的,准确说是分
<script>
块进行的。 -
var 定义的变量和函数声明的方式有提升,函数表达式不提升。
-
在进入执行上下文后,填充 VO 的顺序是: 函数的形参 -> 函数申明 -> 变量申明。所以函数声明会覆盖变量声明,但不会覆盖变量赋值。
// 函数声明会覆盖变量声明 function value() { return 1; } var value; console.log(typeof value); //"function" // 函数声明不会覆盖变量赋值 function value() { return 1; } var value = 1; console.log(typeof value); //"number"
渲染引擎处理网页,通常分成四个阶段。 1. 解析代码:HTML 代码解析为 DOM,CSS 代码解析为 CSSOM(CSS Object Model) 1. 对象合成:将 DOM 和 CSSOM 合成一棵渲染树(render tree) 1. 布局:计算出渲染树的布局(layout) 1. 绘制:将渲染树绘制到屏幕
-
渲染树转换为网页布局,称为“布局流”(flow);布局显示到页面的这个过程,称为“绘制”(paint)。它们都具有阻塞效应,并且会耗费很多时间和计算资源。
-
页面生成以后,脚本操作和样式表操作,都会触发重流(reflow)和重绘(repaint)。用户的互动,也会触发,比如设置了鼠标悬停(a:hover)效果、页面滚动、在输入框中输入文本、改变窗口大小等等。
-
尽量设法降低重绘的次数和成本
- 读取 DOM 或者写入 DOM,尽量写在一起,不要混杂
- 缓存 DOM 信息
- 不要一项一项地改变样式,而是使用 CSS class 一次性改变样式
- 使用 document fragment 操作 DOM
- 动画时使用 absolute 定位或 fixed 定位,这样可以减少对其他元素的影响
- 只在必要时才显示元素
- 使用 window.requestAnimationFrame(),因为它可以把代码推迟到下一次重流时执行,而不是立即要求页面重流
- 使用虚拟 DOM(virtual DOM)库
// 重绘代价高 function doubleHeight(element) { var currentHeight = element.clientHeight; element.style.height = currentHeight * 2 + 'px'; } all_my_elements.forEach(doubleHeight); // 重绘代价低 function doubleHeight(element) { var currentHeight = element.clientHeight; window.requestAnimationFrame(function() { element.style.height = currentHeight * 2 + 'px'; }); } all_my_elements.forEach(doubleHeight);
除了 null
undefined
其余所有变量都可以当作对象使用。数字的字面量也可以,但是使用时要注意。
6.toString(); // 出错:SyntaxError,点操作符被解析为浮点数字面值的一部分
// 正确使用
6..toString(); // 第二个点号可以正常解析
6 .toString(); // 注意点号前面的空格
(6).toString(); // 6 先被计算
6['toString']() // 中括号来访问对象属性
删除对象的指定属性,需要使用 delele
操作符,值改为 undefined
或 null
仅仅是移除了属性和值的关联。
var classTest = obj => Object.prototype.toString.call(obj).slice(8, -1);
classTest(new Number(12)); // Number
- 强制类型转换
Number(str); // 有一个字符不是数值,就是 NaN,比 parseInt() 严格
Number(obj); // 转换为 NaN,除了单数值数组 Number([5]) === 5
Number(obj); // 机制:先调用对象 valueOf(),不是原始类型再调用 toString()
String(arr); // 返回数组的字符串形式 String([1,2,3]) === '1,2,3'
String(obj); // 返回类型字符串 String({}) === '[object Object]'
String(obj); // 机制:先调用对象 toString(),不是原始类型再调用 valueOf()
Boolean(b); // 6个为假: undefined null ±0 NaN '' false
- 自动类型转换
// 1. 有一项为字符串就是字符串拼接
123 + 'abc'; // "123abc"
// 2. 对非布尔值类型的数据求布尔值
if ('abc') {
console.log('hello');
} // "hello"
// 3. 对非数值类型的数据使用一元运算符(即“+”和“-”)
+{ foo: 'bar' } // NaN
+[1, 2, 3] // NaN
// 4. 几个怪癖
+[1] // 1
null + 1 // 1
undefined + 1 // NaN
undefined == 0 // false ,不能和任何数字比较
null == 0 // false ,同上
// 下面的比较结果是:true
new Number(10) == 10; // Number.toString() 返回的字符串被再次转换为数字
10 == '10'; // 字符串被转换为数字
10 == '+10 '; // 同上
10 == '010'; // 同上
isNaN(null) == false; // null 被转换为数字 0,0 当然不是一个 NaN
// 下面的比较结果是:false
10 == 010;
10 == '-10';
// 将一个值加上空字符串可以轻松转换为字符串类型。
'' + 10 === '10'; // true
// 使用一元的加号操作符,可以把字符串转换为数字。
+'10' === 10; // true
原型链实现对原型属性和方法的继承(需要共享的),通过借用构造函数实现对实例属性的继承(不共享的)。即实现了函数复用,而且每个实例拥有自己的属性。
// 父类的实例属性(不共享)
function Supertype(name) {
this.name = name;
this.colors = ['red', 'blue', 'green'];
}
// 父类的原型属性和方法(共享)
Supertype.prototype.company = 'TD'; // 基本类型时各自查询各自的
Supertype.prototype.hobby = ['reading', 'shopping']; //为引用类型时共享(数组时)
Supertype.prototype.sayName = function() {
console.log(this.name);
};
// 构造函数方式继承实例属性(不共享)
function Subtype(name, age) {
Supertype.call(this, name);
this.age = age;
}
// 原型链方式继承原型属性和方法(共享)
Subtype.prototype = new Supertype();
Subtype.prototype.constructor = Subtype;
Subtype.prototype.sayAge = function() {
console.log(this.age);
};
// 实例
var super1 = new Supertype('Alex');
var super2 = new Supertype('Bob');
var sub1 = new Subtype('Candy', 20);
var sub2 = new Subtype('Doom', 30);
super1.sayName(); // Alex
super1.sayAge(); // not function
sub1.sayName(); // Candy
sub1.sayAge(); // 20
sub1.colors.push('black');
console.log(super1.colors); // ["red", "blue", "green"],只影响sub1,其余3个不影响
delete sub1.colors;
console.log(sub1.colors); // 并不是 undefined,而是 ["red", "blue", "green"]
// 删除了从实例属性继承来的colors,读取colors会成为从原型继承来的实例属性
super1.hobby.push('chatting');
console.log(super2.hobby); // ["reading", "shopping", "chatting"],四个都影响
super1.company = 'AM';
console.log(super2.company); // TD
- 优化组合继承:原型链继承时调用 2 次超类,修改为只调用一次的寄生式继承。
- 背后思路是:不必为了指定子类型的原型而调用超类型的构造函数,我们所需要的无非是超类型原型的一个副本而已。
- 本质上:使用寄生式继承来继承超类型的原型,再将结果指定给子类型的原型。
// 之前
Subtype.prototype = new Supertype();
Subtype.prototype.constructor = Subtype;
// 修改为
function inheritPrototype(Subtype, Supertype) {
var prototype = Object.create(Supertype.prototype);
prototype.constructor = Subtype;
Subtype.prototype = prototype;
}
inheritProto(Subtype, Supertype);
for in 遍历原型,不遍历不可枚举 hasOwnProperty 不遍历原型,遍历不可枚举
函数是 JavaScript 中的一等对象
- 一共只有五种情况:
- 全局范围直接使用 this:指向全局对象
- 函数内调用 this:也指向全局对象
- 方法内调用 this:指向调用的对象
- 构造函数内使用 this:指向新创建的对象
- apply call 显式设置 this:显式设置为函数调用的第一个参数
-. 使用时注意点
- 避免多层 this, 内部 若要使用 this,最好将外部 this 固定为 that,内部引用 that 对象。
- 数组处理方法中的 this,如 map 和 foreach,若要使用 this 时,最好考虑清楚是否需要添加 map 的第二个参数 this,固定运行环境时的数组。
- 在回掉函数如添加 dom 事件时,考虑清楚是否需要绑定 this。
- 常见误解一:直接调用函数时,this 指向全局对象,这是语言设计的错误地方!
Foo.method = function() {
function test() {
console.log(this); // this 将会被设置为全局对象
}
test();
};
/* 一个常见的误解是 test 中的 this 将会指向 Foo 对象,实际上不是这样子的。
为了在 test 中获取对 Foo 对象的引用,
我们需要在 method 函数内部创建一个局部变量指向 Foo 对象。
*/
Foo.method = function() {
var that = this;
function test() {
console.log(that); // 使用 that 来指向 Foo 对象
}
test();
};
- 常见误解二:方法的赋值表达式。将一个对象的方法赋值给一个变量,此时新的方法调用,会导致内部的 this 不再指向原对象。
var test = someObject.methodTest;
test(); // 内部的 this 不再指向 someObject
- 常见误解三:当调用括号的左边不是引用类型而是其它类型,这个值自动设置为 null,结果为全局对象。
var foo = {
bar: function() {
console.log(this);
},
};
foo.bar(); // Reference, OK => foo
foo.bar(); // Reference, OK => foo
(foo.bar = foo.bar)(); // global? undefined
(false || foo.bar)(); // global? undefined
(foo.bar, foo.bar)(); // global? undefined
- 创建一个空对象,作为将要返回的对象实例;
- 将这个空对象的原型,指向构造函数的 prototype 属性;
- 将这个空对象赋值给函数内部的 this 关键字;
- 开始执行构造函数内部的代码;
- 如果构造函数内部有 return 语句,而且 return 后面跟着一个对象,new 命令会返回 return 语句指定的对象;否则,就会不管 return 语句,返回 this 对象。
// 类似 new 过程 的 new generator
var newGen = function(/* constructor, param1 */) {
var obj = new Object(); // 从 Object.prototype 上克隆一个空的对象
Constructor = [].shift.call(arguments); // 取得外部传入的构造器
Object.setPrototypeOf(obj, Object.getPrototypeOf(Constructor));
// or obj.__proto__ = Constructor.prototype; // 指向正确的原型
var ret = Constructor.apply(obj, arguments); // 借用外部传入的构造器给 obj 设置属性
return typeof ret === 'object' ? ret : obj; // 确保构造器总是会返回一个对象
};
// or use Object.create()
function _new(/* constructor, param1 */) {
var args = [].slice.call(arguments);
var Constructor = args.shift();
var obj = Object.create(Constructor.prototype);
var ret = Constructor.apply(obj, args);
return typeof ret === 'object' ? ret : obj;
}
function Person(name) {
this.name = name;
}
var person1 = new Person('seven');
var person2 = newGen(Person, 'seven');
比如函数内需要访问 foo 变量时,会按照下面顺序查找 foo:
- 当前作用域内是否有 var foo 的定义;
- 函数形式参数是否有使用 foo 名称的;
- 函数自身是否叫做 foo;
- 回溯到上一级作用域,然后从 #1 重新开始。
- 绝对不能对异步回调函数(即使在数据已经就绪)进行同步调用。
- 如果想在将来某时刻调用异步回调函数的话,可以使用 setTimeout 等异步 API。
- Promise 在规范上规定 Promise 只能使用异步调用方式 。
可迭代协议允许 JavaScript 对象去定义或定制它们的迭代行为, 例如定义在一个 for..of
结构中什么值可以被循环(得到)。一些内置类型都是内置的可迭代对象并且有默认的迭代行为, 比如 Array
String
Map
Set
TypedArray
, 另一些类型则不是 (比如 Object) 。
当一个对象需要被迭代的时候(比如开始用于一个 for..of 循环中),它的@@iterator 方法被调用并且无参数,然后返回一个用于在迭代中获得值的迭代器。
当一个对象被认为是一个迭代器时,它实现了一个 next()
的方法并且拥有以下含义:属性为 next
值为一个包含 done (boolean)
和 value
的对象。
var someArray = [1, 5, 7];
var someArrayValues = someArray.values();
someArrayValues.toString(); // "[object Array Iterator]"
someArrayValues === someArrayValues[Symbol.iterator](); // true
var iterator = someArrayValues[Symbol.iterator]();
iterator + ''; // "[object Array Iterator]"
iterator.next(); // { value: 1, done: false }
iterator.next(); // { value: 5, done: false }
iterator.next(); // { value: 7, done: false }
iterator.next(); // { value: undefined, done: true }
// 自定义可迭代对象
var myIterable = {};
myIterable[Symbol.iterator] = function*() {
yield 1;
yield 2;
yield 3;
};
[...myIterable]; // [1, 2, 3]
// 生成器式的迭代器
function* idMaker() {
var index = 0;
while (true) {
yield index++;
}
}
var gen = idMaker();
console.log(gen.next().value); // '0'
console.log(gen.next().value); // '1'
console.log(gen.next().value); // '2'