1 对象
1.1 六种主要类型
-
string
-
number
-
boolean
-
null
typeof(null) === object
-
undefined
-
object
1.2 属性描述符
- writable
- enumerable
- configurable
- value
1.3 遍历
-
some
-
every
-
forEach
-
for … in
-
for … of
for…of是es6引入的新遍历方式,使用的是迭代器,其原理相当于:
var myObject = { a: 2, b: 3 }; Object.defineProperty( myObject, Symbol.iterator, { enumerable: false, writable: false, configurable: true, value: function() { var o = this; var idx = 0; var ks = Object.keys( o ); return { next: function() { return { value: o[ks[idx++]], done: (idx > ks.length) }; } }; } } ); // 手动遍历 myObject var it = myObject[Symbol.iterator](); var x = it.next(); while(!x.done){ console.log(x.value);; x = it.next(); }
1.4 对象拷贝
-
深拷贝
- JSON.parse(JSON.stringify())
- 递归复制所有层级属性
- $.extend
-
浅拷贝
-
Object.assign()仅对最外一层做了深拷贝,里面的对象仍然是浅拷贝
2 var
-
其创建的全局变量无法删除(delete)
-
无var创建的隐式全局变量可删除(delete)
3 原型链
-
原型 prototype
function Person(){ } Person.prototype = { name : "Nicholas", age : 29, job: "Software Engineer", sayName : function () { alert(this.name); } }; // 此时constructor 属性不再指向 Person,而是指向object : var friend = new Person(); alert(friend instanceof Object); //true alert(friend instanceof Person); //true alert(friend.constructor == Person); //false alert(friend.constructor == Object); //true // 若构造函数很重要则可写成 : // ( 此时constructor 的 Enumerable 会被设置为true ) Person.prototype = { constructor : Person, name : "Nicholas", age : 29, job: "Software Engineer", sayName : function () { alert(this.name); } }; // 默认情况下,原生的 constructor 属性是不可枚举的,因此如果你使用兼容 ECMAScript 5 的 JavaScript 引擎,可以试一试Object.defineProperty() : //重设构造函数,只适用于 ECMAScript 5 兼容的浏览器 Object.defineProperty(Person.prototype, "constructor", { enumerable: false, value: Person });
4 作用域
4.1 异常
function foo(a) {
var b;
console.log( a + b );
b = a;
}
foo( 2 );
这是一个“未声明”的变量,因为在任何相关的作用域中都无法找到它。在所有嵌套的作用域中遍寻不到所需的变量,引擎就会抛出 ReferenceError 异常
-
ReferenceError
在作用域中找不到相应变量就会提示该错误
-
TypeError
变量已声明,但未赋值,导致执行相应操作报错,
如 var a ; a(12);
变量a仅做了声明,但是里面被当做函数来执行
4.2 函数作用域
-
匿名与具名
-
立即执行函数 IIFE
-
解决 undefined 标识符的默认值被错误覆盖导致的异常
将一个参数命名为 undefined,但是在对应的位置不传入任何值,这样就
可以保证在代码块中 undefined 标识符的值真的是 undefined:
undefined = true; // 给其他代码挖了一个大坑!绝对不要这样做! (function IIFE( undefined ) { var a; if (a === undefined) { console.log( "Undefined is safe here!" ); } })();
-
倒置代码的运行顺序
var a = 2;30 (function IIFE( def ) { def( window ); })(function def( global ) { var a = 3; console.log( a ); // 3 console.log( global.a ); // 2 });
-
4.3 块作用域
-
垃圾收集
function process(data) { // 在这里做点有趣的事情 } var someReallyBigData = { .. }; process( someReallyBigData ); var btn = document.getElementById( "my_button" ); btn.addEventListener( "click", function click(evt) { console.log("button clicked"); }, /*capturingPhase=*/false );
click 函数的点击回调并不需要 someReallyBigData 变量。理论上这意味着当 process(…) 执
行后,在内存中占用大量空间的数据结构就可以被垃圾回收了。但是,由于 click 函数形成
了一个覆盖整个作用域的闭包, JavaScript 引擎极有可能依然保存着这个结构(取决于具体
实现)。块作用域可以打消这种顾虑,可以让引擎清楚地知道没有必要继续保存 someReallyBigData 了:
function process(data) { // 在这里做点有趣的事情 } // 在这个块中定义的内容完事可以销毁! { let someReallyBigData = { .. }; process( someReallyBigData ); } var btn = document.getElementById( "my_button" ); btn.addEventListener( "click", function click(evt){ console.log("button clicked"); }, /*capturingPhase=*/false );
5 函数
5.1 函数参数
-
所有函数的参数都是按值传递的
Example:
function setName(obj) { obj.name = "Nicholas"; obj = new Object(); obj.name = "Greg"; } var person = new Object(); setName(person); alert(person.name); //"Nicholas"
参数按值传递,执行setName函数时会将person所指向的Object实例地址复制一份,传到参数内部。该副本及其person所指向的都是同一个内存区域,即他们存储的指针地址都一样。
5.2 命名函数表达式
- 声明
- 表达式
5.3 隐式转换
// 栗子1:
function fn() {
return 20;
}
console.log(fn + 10);
// 栗子2:
function fn() {
return 20;
}
fn.toString = function() {
return 10;
}
console.log(fn + 10); // 输出结果是多少?
// 栗子3:
function fn() {
return 20;
}
fn.toString = function() {
return 10;
}
fn.valueOf = function() {
return 5;
}
console.log(fn + 10);
- 当我们没有重新定义toString与valueOf时,函数的隐式转换会调用默认的toString方法,它会将函数的定义内容作为字符串返回。而当我们主动定义了toString/vauleOf方法时,那么隐式转换的返回结果则由我们自己控制了。其中valueOf的优先级会toString高一点。
5.4 函数式编程
-
柯里化
-
多参数的函数转换成单参数的形式
function currying(fn, n) { return function (m) { return fn.call(this, m, n); }; }
-
6 变量
6.1 NaN
console.log(NaN == NaN); // false
console.log(NaN === NaN); // false
alert(isNaN(true)); //false(可以被转换成数值 1)
6.2 null 与 undefined
-
null 不是空对象指针,而是基本数据类型,故有:
typeof(null) // object
在 JavaScript 中二进制前三位都为 0 的话会被判
断为 object 类型, null 的二进制表示是全 0,自然前三位也是 0,所以执行 typeof 时会返回“object”
-
undefined 派生自 null
console.log(null == undefined); //true console.log(null === undefined); //false
7 闭包
闭包就是能够读取其他函数内部变量的函数。
// 栗子1:
// getNameFunc返回的匿名函数的执行环境是全局的,而且this只在函数内部起作用。此时的this.name在匿名函数中找不到,所以就从全局中找,找到后打印出来。
var name = "The Window";
var object = {
name : "My Object",
getNameFunc : function(){
return function(){
return this.name;
};
}
};
alert(object.getNameFunc()());
// 栗子2:
var name = "The Window";
var object = {
name : "My Object",
getNameFunc : function(){
var that = this;
return function(){
return that.name;
};
}
};
alert(object.getNameFunc()());
7.1 用处
- 读取函数内部变量
- 让这些变量始终保持在内存中
// 栗子1:
function f1(){
var n=999;
nAdd=function(){n+=1}
function f2(){
alert(n);
}
return f2;
}
var result=f1();
result(); // 999
nAdd();
result(); // 1000
// 原因就在于f1是f2的父函数,而f2被赋给了一个全局变量,这导致f2始终在内存中,而f2的存在依赖于f1,因此f1也始终在内存中,不会在调用结束后,被垃圾回收机制(garbage collection)回收。
// 栗子2:
for( var i = 0; i < 5; i++ ) {
setTimeout(() => {
console.log( i );
}, 1000 * i)
}
// 结果为5个5,每秒输出一个
// 根据作用域的工作原理,实际情况是尽管循环中的五个函数是在各个迭代中分别定义的,
// 但是它们都被封闭在一个共享的全局作用域中,因此实际上只有一个 i。
// 栗子3:
for (var i = 1; i <= 5; i++) {
let j = i; // 输出结果正常
setTimeout(function timer() {
console.log(j);
},j*1000);
}
// 栗子4:
for (var i = 1; i <= 5; i++) {
var j = i; // 输出结果仍然是5个5
setTimeout(function timer() {
console.log(j);
},j*1000);
}
// 栗子5:
for( var i = 0; i < 5; i++ ) {
((j) => {
setTimeout(() => {
console.log( j );
}, 1000 * j)
})(i)
}
// 栗子6:
function test(){
for (let i=0; i<5; i++) { // 正常
setTimeout( function timer() {
console.log(new Date(),i);
}, i*1000 );
}
// console.log("end",new Date(),i); //因为变量作用域的问题,这里会报i 不存在,未声明
}
7.2 注意
- 由于闭包会使得函数中的变量都被保存在内存中,内存消耗很大,
所以不能滥用闭包,否则会造成网页的性能问题,在IE中可能导致内存泄露。
解决方法是,在退出函数之前,将不使用的局部变量全部删除。 - 闭包会在父函数外部,改变父函数内部变量的值。所以,如果你把父函数当作对象(object)使用,把闭包当作它的公用方法(Public Method),把内部变量当作它的私有属性(private value),这时一定要小心,不要随便改变父函数内部变量的值。
8 改变环境上下文
8.1 apply/call
都是为了改变函数执行上下文
-
改变this指针,第一个参数都是this要指向的对象,也就是想指定的上下文;
-
借用其他对象的方法
下面就借用一道面试题,来更深入的去理解下 apply 和 call 。
定义一个 log 方法,让它可以代理 console.log 方法,常见的解决方法是:
function log(msg) {
console.log(msg);
}
log(1); //1
log(1,2); //1
// 上面方法可以解决最基本的需求,但是当传入参数的个数是不确定的时候,上面的方法就失效了,这个时候就可以考虑使用 apply 或者 call,注意这里传入多少个参数是不确定的,所以使用apply是最好的,方法如下:
function log(){
console.log.apply(console, arguments);
};
log(1); //1
log(1,2); //1 2
// 接下来的要求是给每一个 log 消息添加一个"(app)"的前辍,比如:
log("hello world"); //(app)hello world
// 该怎么做比较优雅呢?这个时候需要想到arguments参数是个伪数组,通过 Array.prototype.slice.call 转化为标准数组,再使用数组方法unshift,像这样:
function log(){
var args = Array.prototype.slice.call(arguments);
args.unshift('(app)');
console.log.apply(console, args);
};
-
apply
func.apply(this, [arg1, arg2])
-
call
func.call(this, arg1, arg2);
-
bind
绑定后返回一个函数,待后续调用。
多次 bind() 是无效的,更深层次的原因, bind() 的实现,相当于使用函数在内部包了一个 call / apply ,第二次 bind() 相当于再包住第一次 bind() ,故第二次以后的 bind 是无法生效的。
9 声明提升
9.1 编译器执行流程
-
- 编译阶段,定义声明
-
- 执行阶段,保留原地
9.2 函数及变量声明都会被提升
foo();
function foo() {
console.log( a ); // undefined
var a = 2;
}
// 函数声明会被提升,但是函数表达式却不会被提升。
foo(); // 不是 ReferenceError, 而是 TypeError!
var foo = function bar() {
// ...
};
// 函数表达式不会被提升
// 函数声明会被提升,但是函数表达式却不会被提升。
foo(); // 不是 ReferenceError, 而是 TypeError!
var foo = function bar() {
// ...
};
// 即使是具名的函数表达式,名称标识符在赋值之前也无法在所在作用域中使用:
foo(); // TypeError
bar(); // ReferenceError
var foo = function bar() {
// ...
};
// 使用let定义的变量不会被提升
// 函数声明优先被提升
foo(); // 1
var foo;
function foo() {
console.log( 1 );
}
foo = function() {
console.log( 2 );
};
// 会输出 1 而不是 2 !这个代码片段会被引擎理解为如下形式:
function foo() {
console.log( 1 );
}
foo(); // 1
foo = function() {
console.log( 2 );
};
// 注意, var foo 尽管出现在 function foo()... 的声明之前,但它是重复的声明(因此被忽略了),因为函数声明会被提升到普通变量之前。
9.3 被条件判断所控制的函数声明
foo(); // "b"
var a = true;
if (a) {
function foo() { console.log("a"); }
}
else {
function foo() { console.log("b"); }
}
10 模块化规范
10.1 CommonJS
以同步方式加载模块,主要用在后端。
var clock = require('clock');
clock.start();
这种写法适合服务端,因为在服务器读取模块都是在本地磁盘,加载速度很快。但是如果在客户端,加载模块的时候有可能出现“假死”状况。
-
缺陷
- 没有模块系统
- 标准库较少
- 没有标准接口
- 缺乏包管理系统
10.2 AMD(依赖前置)
异步的加载模块,Asynchronous Module Definition:
require(['clock'],function(clock){
clock.start();
});
AMD虽然实现了异步加载,但是开始就把所有依赖写出来是不符合书写的逻辑顺序的,能不能像commonJS那样用的时候再require,而且还支持异步加载后再执行呢?所以才有了CMD
CMD(依赖就近)
由国内的玉伯提出,CMD (Common Module Definition), 是seajs推崇的规范,CMD则是依赖就近,用的时候再require。它写起来是这样的:
define(function(require, exports, module) {
var clock = require('clock');
clock.start();
});
AMD和CMD最大的区别是对依赖模块的执行时机处理不同,而不是加载的时机或者方式不同,二者皆为异步加载模块。
11 this
绑定规则
- 默认绑定
- 隐式绑定
function foo() {
console.log( this.a );
}
var obj = {
a: 2,
foo: foo
};
obj.foo(); // 2
function foo() {
console.log( this.a );
}
var obj2 = {
a: 42,
foo: foo
};
var obj1 = {
a: 2,
obj2: obj2
};
obj1.obj2.foo(); // 42
// 隐式绑定丢失,退回默认板顶的栗子1:
function foo() {
console.log( this.a );
}
var obj = {
a: 2,
foo: foo
};
var bar = obj.foo; // 函数别名!
var a = "oops, global"; // a 是全局对象的属性
bar(); // "oops, global"
// 栗子2:
function foo() {
console.log( this.a );
}
function doFoo(fn) {
// fn 其实引用的是 foo
fn(); // <-- 调用位置!
}
var obj = {
a: 2,
foo: foo
};
var a = "oops, global"; // a 是全局对象的属性
doFoo( obj.foo ); // "oops, global"
/// 栗子3:
function foo() {
console.log( this.a );
}
var obj = {
a: 2,
foo: foo
};
var a = "oops, global"; // a 是全局对象的属性
setTimeout( obj.foo, 100 ); // "oops, global"
- 显式绑定
- new绑定
12 Event loop(事件循环)
12.1 同步任务
12.2 异步任务
-
微观任务
- Promise
- process.nextTick
- MutationObserver
-
宏观任务
- setTimeout
- setInterval
- setImmediate
- UI rendering
- I/O