共计 9827 个字符,预计需要花费 25 分钟才能阅读完成。
把知识串一串,连成线,形成体系,从此走上大神之路啦,道路可能会曲折一点,但是咸鱼也要翻一翻身撒~
一、变量提升
何为变量提升?
在 JavaScript 中,函数及变量的声明都将被提升到函数的最顶部 (函数声明的优先级高于变量声明的优先级)
这样就造成了一种不同于其他语言的现象,初看甚至觉得有些诡异:变量可以先使用再声明。举个栗子:
x = 1;
console.log(x); // 1
var x;
var name = 'World!';
(function () {if (typeof name === 'undefined') {
var name = 'Jack';
console.log('Goodbye' + name);
} else {console.log('Hello' + name);
}
})();
// 输出为 Goodbye Jack
为什么会出现这样情况呢?
在 JavaScript 中,变量声明与赋值的分离,如 var a = 2 这个代码是分两步进行的,编译阶段之行变量声明 var a,在执行阶段进行赋值 a = 2,于是便造成了了变量声明提前情况的发生。
解析:对于第二个例子,由于存在变量提升,所以变量声明先于 if 判断,所以此时 name = undefined,于是便输出了 Goodbye Jack
二、隐式转换
前段时间,前端各大博客被一道题刷屏了
++[[]][+[]]+[+[]]==10?
这道题怎么去解决呢,这就涉及到了 JS 的隐式转换相关的知识了。
简述隐式转换规则
对于原始类型:Undefined、Null、Boolean、Number、String
1,加号运算符(+):若后面的是数字,会直接相加得出结果,如 1 + 1 = 2;若后面的是字符类型,则会进行字符拼接,如 1 + ‘1’ = ’11’。
2,减号运算符(-):若后面的是数字,会直接相减得出结果;若后面的字符,则会将其转为数字类型,然后相减得出结果。
3,== 运算负责:
- undefined == null,结果为 true
- String == Boolean,需要将两个操作数同时转化为 Number
- String/Boolean == Number,需要将 String/Boolean 转为 Number
对于对象类型:Object
当对象与一个非对象进行比较等操作时,需要先将其转化为原始类型:首先调用 valueOf(),若结果是原始类型,则返回结果;若结果不是原始类型,则继续调用 toSring(),返回其结果,若结果依然不是原始类型,则会抛出一个类型错误。
这里有一道很火的面试题,就是利用对象的类型转换原理:
a == 1 && a == 2 && a == 3
// 答案:var a = {num : 0};
a.valueOf = function() {return ++a.num;}
以上大概为基础的隐式转换规则,可能不太完善,欢迎大家留言补充。好,有了这些准备后,让我们再来看下一开始的题目,让我们来逐步拆解:
1,根据运算符的优先级,我们可以得到:(++[[]][+[]])+[+[]]
2,根据隐式转换,我们得到:(++[[]][0])+[0]
3,再次简化:(++[]) + [0]
4,这个时候就很明朗了,最终划为字符拼接 '1' + '0' = '10';
三,闭包
什么是闭包?
简单的讲,闭包就是指有权访问另一个函数作用域中的变量的函数。
MDN 上面这么说:闭包是一种特殊的对象,是函数和声明该函数的词法环境的组合。
产生一个闭包
function func() {
var a = 1;
return function fn() {console.log(a);
}
}
func()(); // 1
这里函数 func 在调用后,其作用域并没有被销毁,依然可以被函数 fn 访问,所以输出为 1。
这里有道很经典的面试题
function fun(n,o){console.log(o);
return {fun: function(m){return fun(m,n);
}
};
}
var a = fun(0); // ?
a.fun(1); // ?
a.fun(2); // ?
a.fun(3); // ?
var b = fun(0).fun(1).fun(2).fun(3); // ?
var c = fun(0).fun(1); // ?
c.fun(2); // ?
c.fun(3); // ?
undefined
0
0
0
undefined, 0, 1, 2
undefined, 0
1
1
哈哈,有点绕,有兴趣的同学可以简单看下。
四,深、浅克隆
在实际开发或面试中,我们经常会碰到克隆的问题,这里我们简单的总结下。
浅克隆
浅克隆就是复制对象的引用,复制后的对象指向的都是同一个对象的引用,彼此之间的操作会互相影响
var a = [1,2,3];
var b = a;
b[3] = 4;
console.log(a, b);
// [1,2,3,4] [1,2,3,4]
实际开发中,若需要同步对象的变化,往往用的就是浅克隆,直接复制对象引用即可。
深克隆
开发过程中,我们往往需要断开对象引用,不影响原对象,这个时候我们就用到深克隆了,有如下方法:
方法一
JSON.parse(JSON.stringify()),对于大多数情况都可以用这种方法解决,一步到位。但是若对象中存在正则表达式类型、函数类型等的话,会出现问题:会直接丢失相应的值,同时如果对象中存在循环引用的情况也无法正确处理
let a = {name: '小明'};
let b = JSON.parse(JSON.stringify(a));
b.age = 18;
console.log(a, b);
// {name: '小明'} {name: "小明", age: 18}
方法二
对于数组,我们可以利用 Array 的 slice 和 concat 方法来实现深克隆
let a = [1,2,3];
let b = a.slice();
b.push(4);
console.log(a, b);
// [1,2,3] [1,2,3,4]
let a1 = [1,2,3];
let b1 = a.concat(4);
b1.push(5);
console.log(a, b);
// [1,2,3] [1,2,3,4,5]
方法三
jQuery 中的 extend 复制方法:$.extend(true, target, obj)
let a = {name: '小明'};
let b = {}
$.extend(true, b, a);
b.age = 18;
console.log(a, b);
// {name: "小明"} {name: "小明", age: 18}
五、this 指向
关于 this 指向的问题,这里是有一定判断方法的:
位置:this 实际上是在函数被调用时发生的绑定,它指向什么完全取决于函数在哪里调用
规则:默认绑定、隐式绑定、显式绑定、new 绑定
我们在实际判断的时候,需要将二者结合起来。
1,默认规则
var name = '小明';
function print() {console.log(this.name); // '小明'
console.log(this); //window 对象
}
print();
// '小明'
解析:print()直接使用不带任何修饰的函数引用进行的调用,这个时候只能使用默认绑定规则,即 this 指向全局对象,所以此题输出为:’ 小明 ’
2,隐式绑定
function foo() {console.log(this.a)
}
var obj = {
a:2,
foo:foo
}
obj.foo() // 2
解析:当函数引用有上下文对象时,隐式绑定规则会把函数调用中的 this 绑定到这个上下文对象。所以此题的 this 被绑定到 obj,于是 this.a 和 obj.a 是一样的。
这里有两点点需要注意:
1,对象属性引用链中只有上一层或者说最后一层在调用位置中起作用,举例如下:
function foo() {console.log(this.a);
}
var obj2 = {
a: 10,
foo: foo
}
var obj1 = {
a: 2,
obj2: obj2
}
obj1.obj2.foo(); // 10
2,隐式丢失:被隐式绑定的函数会丢失绑定对象,也就是说它会应用默认绑定,从而把 this 绑定到全局对象或者 undefined 上。举例如下:
var a = 'hello world';
function foo(){console.log(this.a)
}
var obj = {
a:1,
foo:foo
}
var print = obj.foo;
print(); // hello world
解析:虽然 print 是 obj.foo 的一个引用,但是实际上,它引用的是 foo 函数本身,所以此时 print()其实是一个不带任何修饰的函数调用,应用了隐式绑定。
3,显示绑定
利用 call(),apply(),bind()强制绑定 this 指向的我们称之为显示绑定,举例如下:
function foo() {console.log(this.a);
}
var obj = {a:1}
foo.call(obj); // 1
这里有一点需要注意:显示绑定依然无法解决上面提到的丢失绑定问题。举例如下:
var a = 'hello world';
function foo(){console.log(this.a)
}
var obj = {
a:1,
foo:foo
}
var print = obj.foo;
print.bind(obj)
print(); // hello world
这里有关 call、apply、bind 的具体用法就不再一一阐述了,后面的部分会详细讲解。
4,new 绑定
这是最后一条 this 的绑定规则,使用 new 来调用函数,或者说发生构造函数调用时,会执行下面的操作:
- 创建 (或者说构造) 一个全新的对象
- 这个新对象会被执行 [[Prototype]] 连接
- 这个新对象会绑定到函数调用的 this
- 如果函数没有返回其他对象,那么 new 表达式中的函数调用会自动返回这个新对象。
这个过程中发生了 this 绑定,举例如下:
function Person(name) {this.name = name;}
var p = new Person('小明');
console.log(p.name); // 小明
5,优先级
这里不再一一举例对比优先级,直接给出结论:new 绑定 > 显示绑定 > 隐式绑定 > 默认绑定,有兴趣的同学可以实际比对一下。
常规 this 指向判断流程:
- 函数是否在 new 中调用(new 绑定)?如果是的话 this 绑定的就是新创建的对象
- 函数是否通过 call、apply、bind(显示绑定) ? 如果是的话,this 绑定的是指定的对象
- 函数是否在某个上下文对象中被调用(隐时绑定) ? 如果是的话,this 绑定的是那个上下文对象
- 如果都不是的话,使用默认绑定
六、call、apply、bind
1,call()
定义:
使用一个指定的 this 值和单独给出的一个或多个参数来调用一个函数
语法:
fun.call(thisArg, arg1, arg2, …)
参数:
thisArg:(1) 不传,或者传 null,undefined,函数中的 this 指向 window 对象
(2) 传递另一个函数的函数名,函数中的 this 指向这个函数的引用,并不一定是该函数执行时真正的 this 值
(3) 值为原始值(数字,字符串,布尔值) 的 this 会指向该原始值的自动包装对象,如 String、Number、Boolean(4)传递一个对象,函数中的 this 指向这个对象arg1, arg2, …:指定的参数列表
举例如下:
var obj = {a: '小明'};
function print() {console.log(this);
}
print.call(obj); // {a: '小明'}
实现 call 方法:
Function.prototype.selfCall = function(context, ...args) {
let fn = this;
context || (context = window);
if (typeof fn !== 'function') throw new TypeError('this is not function');
let caller = Symbol('caller');
context[caller] = fn;
let res = context[caller](...args);
delete context[caller];
return res;
}
2,apply()
apply()方法与 call()方法相似,区别在于:call 方法接受的是若干个参数列表,而 apply 接收的是一个包含多个参数的数组。
举例如下:
var arr = [34,5,3,6,54,6,-67,5,7,6,-8,687];
Math.max.apply(Math, arr); // 687
Math.min.call(Math, ...arr); // -67
3,bind()
定义:
bind()方法创建一个新的函数,在调用时设置 this 关键字为提供的值。并在调用新函数时,将给定参数列表作为原函数的参数序列的前若干项。
语法:
function.bind(thisArg[, arg1[, arg2[, …]]])
参数:
thisArg:调用绑定函数时作为 this 参数传递给目标函数的值
arg1, arg2, …:当目标函数被调用时,预先添加到绑定函数的参数列表中的参数。
举例如下:
function print() {console.log(this);
}
let obj = {name: '小明'};
let fn = print.bind(obj);
fn(); // {name: "小明"}
七、Promise
1,什么是 Promise?
Promise 是 JS 异步编程中的重要概念,异步抽象处理对象,是目前比较流行 Javascript 异步编程解决方案之一
2,创建 Promise
方法一:new Promise
// 声明 Promise 后会立即执行
var promise = new Promise(function(resolve, reject) {resolve('Hello');
})
console.log(promise); // Promise{<resolved>: "Hello"}
方法二:直接创建
var promise = Promise.resolve('Hello');
console.log(promise); // Promise{<resolved>: "Hello"}
3,Promise 状态
promise 相当于一个状态机,具有三种状态:
- pending
- fulfilled
- rejected
(1) promise 对象初始化状态为 pending
(2) 当调用 resolve(成功),会由 pending => fulfilled
(3) 当调用 reject(失败),会由 pending => rejected
注:promsie 状态 只能由 pending => fulfilled/rejected, 一旦修改就不能再变
4,Promise API
1,Promise.prototype.then()
then() 方法返回一个 Promise。它最多需要有两个参数:Promise 的成功 (onFulfilled) 和 失败情况 (onRejected) 的回调函数。
举例如下:
var promise = new Promise((resolve, reject) => {
// 成功
resolve('hello');
});
promise.then((res) => {console.log(res); // hello
return Promise.reject('error');
}).then((success) => {console.log('success', success);
}, (err) => {console.log('error', err); // error error
});
2,Promise.resolve()
Promise.resolve(value)方法返回一个以给定值解析后的 Promise 对象。但如果这个值是个 thenable(即带有 then 方法),返回的 promise 会“跟随”这个 thenable 的对象,采用它的最终状态(指 resolved/rejected/pending/settled);如果传入的 value 本身就是 promise 对象,则该对象作为 Promise.resolve 方法的返回值返回;否则以该值为成功状态返回 promise 对象。
举例如下:
var promise = Promise.resolve('hello');
promise.then((res) => {console.log(res);
});
// hello
// Promise {<resolved>: undefined}
此时 promise 的状态为题 fulfilled
3,Promise.reject()
Promise.reject(reason)方法返回一个带有拒绝原因 reason 参数的 Promise 对象。
举例如下:
var promise = Promise.reject('error');
promise.then((res) => {console.log('success', res);
}, (res) => {console.log('error', res); // error error
});
4,Promise.race()
Promise.race(iterable) 方法返回一个 promise,一旦迭代器中的某个 promise 解决或拒绝,返回的 promise 就会解决或拒绝。
举例如下:
var promise1 = new Promise(function(resolve, reject) {setTimeout(resolve, 500, 'one');
});
var promise2 = new Promise(function(resolve, reject) {setTimeout(resolve, 100, 'two');
});
Promise.race([promise1, promise2]).then(function(value) {console.log(value); // two
});
5,Promise.all()
Promise.all(iterable) 方法返回一个 Promise 实例,此实例在 iterable 参数内所有的 promise 都“完成(resolved)”或参数中不包含 promise 时回调完成(resolve);如果参数中 promise 有一个失败(rejected),此实例回调失败(reject),失败原因的是第一个失败 promise 的结果。
举例如下:
var promise1 = Promise.resolve(3);
var promise2 = 42;
var promise3 = new Promise(function(resolve, reject) {setTimeout(resolve, 100, 'foo');
});
Promise.all([promise1, promise2, promise3]).then(function(values) {console.log(values); // [3, 42, "foo"]
});
6,Promise.prototype.finally()
finally() 方法返回一个 Promise。在 promise 结束时,无论结果是 fulfilled 或者是 rejected,都会执行指定的回调函数。这为在 Promise 是否成功完成后都需要执行的代码提供了一种方式。
这避免了同样的语句需要在 then()和 catch()中各写一次的情况。
举例如下:
var promise = Promise.resolve('Hello');
promise.then((res) => {console.log(res); // Hello
}).finally((res) => {console.log('finally'); // finally
})
7,Promise.prototype.catch()
catch() 方法返回一个 Promise,并且处理拒绝的情况,捕获前面 then 中发送的异常
只要 Promsie 状态更改为 reject 或者抛出异常,都会进入 catch 方法。举例如下:
var promise1 = Promise.reject('Hello');
promise1.then((res) => {console.log('success' + res);
}).catch((res) => {console.log('catch' + res); // catch Hello
})
八、Event Loop
1,前言
Event Loop 即事件循环,是指浏览器或 Node 的一种解决 javaScript 单线程运行时不会阻塞的一种机制,也就是我们经常使用异步的原理。
2,宏任务与微任务
在 JavaScript 中,任务被分为两种,一种宏任务(MacroTask)也叫 Task,一种叫微任务(MicroTask)。
宏任务:
- script 全部代码
- setTimeout
- setInterval
- setImmediate (Node 独有)
- I/O
- UI rendering (浏览器独有)
微任务:
- process.nextTick (Node 独有)
- Promise
- Object.observe
- MutationObserver
3,浏览器的 Event Loop
浏览器中的事件循环机制是什么样子呢?不废话,直接上图:
过程如下:
- 执行全局 Script 同步代码,这些同步代码有一些是同步语句,有一些是异步语句(比如 setTimeout 等);
- 全局 Script 代码执行完毕后,调用栈 Stack 会清空;
- 检查微任务队列是否为空,若不为空,则取出位于队首的回调任务,放入调用栈 Stack 中执行,队列长度减 1。如此循环往复,直至微任务队列为空
- 微任务队列为空后,检查宏任务队列是否为空,若不为空,则取出宏队列中位于队首的任务,放入 Stack 中执行,队列长度减 1。如此循环往复,直至宏任务队列为空
举例如下:
console.log('script start');
setTimeout(function() {console.log('setTimeout');
}, 0);
Promise.resolve().then(function() {console.log('promise1');
}).then(function() {console.log('promise2');
});
console.log('script end');
答案如下:
script start、script end、promise1、promise2、setTimeout
解析:
step1
console.log('script start');
Stack Queue: [console]
Macrotask Queue: []
Microtask Queue: []
打印结果:1
step2
setTimeout(function() {console.log('setTimeout');
}, 0);
setTimeout 属于宏任务,所以:
Stack Queue: [setTimeout]
Macrotask Queue: [callback1]
Microtask Queue: []
step3
Promise.resolve().then(function() {console.log('promise1');
}).then(function() {console.log('promise2');
});
promise 属于微任务,所以有:
Stack Queue: [promise]
Macrotask Queue: [callback1]
Microtask Queue: [callback2]
step4
console.log('script end');
同步任务,直接执行
打印结果:script end
step5
遍历微任务队列:Microtask Queue: [callback2],执行其函数
打印顺序依次为:promise1、promise2
step6
微任务队列为空后,遍历宏任务队列:Macrotask Queue: [callback1],执行其回调函数
打印结果:setTimeout
所以最终结果为:script start、script end、promise1、promise2、setTimeout
九、总结
由于时间比较仓促,本次总结还存在着许多遗漏,如 JS 原型,node 环境下的 Event Loop,函数柯里化等,也有许多理解不到位的情况,日后会逐渐完善与补充。
注:如果文章中有不准确的地方,欢迎大家留言交流。????
作者:易企秀——易小星