写在最前
本文针对2类人群。
- 面试总挂在js基础上的人
- 想夯实js基础 我这也算一个JS基础的体系了。 既然是体系肯定比盲目去网上捞资源来得靠谱,放心入坑。
了解一门语言先从数据类型开始
Q1: js有哪些数据类型? 6种还是8种?
答: 直接上张图你就明白了。
喏,你是不是回答漏了其中一两个?7 种基础类型, 1 种引用类型。请注意以下两点:
基础类型存储在栈内存。被引用或拷贝时,会创建一个完全相等的变量;
引用类型存储在堆内存。存储的是地址,多个引用指向同一个地址。
Q2: 你知道哪些判断数据类型的方法?
typeof
看代码:
代码语言:javascript复制typeof null // 'object'
typeof 1 // 'number'
typeof '1' // 'string'
typeof undefined // 'undefined'
typeof true // 'boolean'
typeof Symbol() // 'symbol'
复制代码
噢吼,虽然知道typeof 可以用来判断基本数据类型,但为啥 typeof null === 'object'?
再看代码:
代码语言:javascript复制Object.prototype.__proto__
Object.prototype.__proto__.a
复制代码
打开你的控制台,输入以下,你会得到:
所以得到结论:null 可以是顶层对象Object的上层描述,但null也是‘objcet’,因为null在Object的链上。 所以判断变量是否为null,请直接 使用 xxx === null 判断。
instanceof
好吧,继续看代码
代码语言:javascript复制const A = function() {}
let a = new A()
a instanceof A // true
const str1 = new String('字符串')
str1 instanceof String // true
const str2 = '字符串'
str2 instanceof String // false
复制代码
仔细看,str2 instanceof String === false。 可以得出以下结论:
typeof 能判断 除null 外的基础数据类型 和 function
instanceof 能判断复杂引用数据类型,不能判断基础数据类型
同意下滑。
Object.prototype.toString
直接访问Object的原型方法 tostring 。 我给你上代码了:
代码语言:javascript复制Object.prototype.toString.call(window) // ‘[object Window]’
Object.prototype.toString.call(document) // ’[object HTMLDocument]‘
Object.prototype.toString.call(null) //‘[object Null]’
Object.prototype.toString({}) // ‘[object Object]’
Object.prototype.toString.call({}) // ‘[object Object]’
Object.prototype.toString.call(10086) // ‘[object Number]’
Object.prototype.toString.call('10086') // ‘[object String]’
Object.prototype.toString.call(true) // ‘[object Boolean]’
Object.prototype.toString.call(()=>{}) // ‘[object Function]’
Object.prototype.toString.call(undefined) // ‘[object Undefined]’
Object.prototype.toString.call(/re g/) // ‘[object RegExp]’
Object.prototype.toString.call(new Date()) // ‘[object Date]‘
Object.prototype.toString.call([]) // ’[object Array]’
复制代码
所以,我相信你肯定看明白了。 总结成一个方法就是:
代码语言:javascript复制function validityType(obj){
let type = typeof obj;
if (type !== "object") { // 基础数据类型,直接返回
return type;
}
return Object.prototype.toString.call(obj).slice(8,-1)
}
复制代码
为什么还要用typeof ? 因为 typeof 性能上优于 toString
强制类型转换
Number()
代码语言:javascript复制Number(true); // 1
Number(false); // 0
Number('00010086'); // 10086
Number(null); // 0
Number(''); // 0
Number('1a'); // NaN
Number('0X11') // 34
Number(-0X22); // -34
复制代码
我相信你又懂了。 读书百遍不如自己亲自验证一下?
Boolean()
代码语言:javascript复制Boolean({}) // true
Boolean(0) // false
Boolean(null) // false
Boolean(undefined) // false
Boolean(NaN) // false
Boolean(10086) // true
Boolean('12') // true
复制代码
号 隐式转换
代码语言:javascript复制1 10085 // 10086
'1' '10085' // '110085'
'1' true // "1true"
'1' undefined // "1undefined"
'1' null // "1null"
'1' 10086n // '110086' 字符串 BigInt,BigInt被转换为字符串
1 undefined // NaN undefined转换数字
1 null // 1 null转换为0
1 true // 2 true转换为1
1 1n // 错误 VM995:1 Uncaught TypeError: Cannot mix BigInt and other types, use explicit conversions
'1' {} // '1[object Object]'
1 {} // '1[object Object]'
'1' ()=>{} // Uncaught SyntaxError: Malformed arrow function parameter list
复制代码
Object
Q2: 1 {} = '1object Object' 对吗? 控制台试试?
代码语言:javascript复制const obj = {
value: 0,
valueOf() {
return 10085;
},
toString() {
return '10087'
},
[Symbol.toPrimitive]() {
return 10086
}
}
console.log(obj 1); // 10087
10086 {} // '10[object Object]'
[1,2,undefined,4,5] 10086; // '1,2,,4,510086'
复制代码
好吧。 这个解释不能再精简了,还是得写写。
代码语言:javascript复制console.log(obj 1); // 10087 因为obj有Symbol.toPrimitive方法,如果木有它则执行valueOf
10 {}; // "10[object Object]",{}会默认调用valueOf是{},不会进行基础类型继续转换。 接着调用{}的toString方法,返回"[object Object]",
[1,2,undefined,4,5] 10086; // [1,2,undefined,4,5]会默认先调用valueOf结果还是这个数组,不是基础数据类型继续转换,接着调用toString 得到"1,2,,4,5",然后再和10086进行运算得到最终结果
复制代码
最后学废了嘛?
new、apply、call、bind 的实现
new
如果你【new】 一下,会经历:
- 创建一个新对象(Object);
- 将构造函数的作用域赋给新对象(也就是 this 要指向新对象);
- 执行构造函数中的代码(给对象挂上属性);
- return newObject。但实际上这个过程背地里要做以下三件事情。
让new出来的实例拥有访问私有成员属性的权限
实例可以访问构造函数原型所在原型链上的属性(也就是 constructor.prototype 上的属性);、
构造函数返回的对象必须是引用数据类型
上个代码,其实不想上,网上答案一大堆。 但是还是希望有空自己手写一下……
代码语言:javascript复制function _new(_function, ...args) {
if(typeof _function !== 'function') {
throw '_function must be a function';
}
let obj = new Object();
obj.__proto__ = Object.create(_function.prototype);
let res = _function.apply(obj, [...args]);
let isObject = typeof res === 'object' && res !== null;
let isFunction = typeof res === 'function';
return isObject || isFunction ? res : obj;
};
复制代码
call / apply
先给个提醒,大家来找茬。
代码语言:javascript复制Function.prototype.call = function (context, ...args) {
const context = context || window;
context.fn = this;
const result = eval('context.fn(...args)');
delete context.fn
return result;
}
Function.prototype.apply = function (context, args) {
const context = context || window;
context.fn = this;
const result = eval('context.fn(...args)');
delete context.fn
return result;
}
复制代码
仔细看看,除了入参有一点点区别,其它就好像一样噢? 其中 eval 是为了立即执行。
bind
代码语言:javascript复制Function.prototype.bind = function (context, ...args) {
if (typeof this !== "function") {
throw new Error("this must be a function");
}
const self = this;
const res = function () {
self.apply(this instanceof self ? this : context, args.concat(Array.prototype.slice.call(arguments)));
}
if(this.prototype) {
res.prototype = Object.create(this.prototype);
}
return res;
}
复制代码
唉。。 还是稍微解释一下。 bind 和 call、apply的区别就在于一个要返回函数,另外两个需要返回eval的执行结果。
js 闭包
我接触过很多面试求职者, 没500也有800了吧。 闭包往往是我想快速结束无聊的面试环节而抛出的一个问题。
Q3: 啥是闭包?
答案1: 函数里面内嵌一个函数
答案2: 内部函数可以访问外部函数的属性
答案3: 函数 return 一个函数
所以,你觉得这两种答案对吗? 别着急回,因为可能回光速打脸。
看代码:
代码语言:javascript复制var f1;
function f2() {
var f1 = 2
f2 = function() {
console.log(f1);
}
}
f1();
f2();
复制代码
这个看起来有点怪? 为什么 能在最外层直接访问 f2 ? 然后好像f2 内部又访问了 f1? 这个 f1 是哪个f1?
想不清楚就再来看个例子:
代码语言:javascript复制var a = 1;
function foo(){
var a = 2;
function inner(){
console.log(a);
}
bar(inner);
}
function bar(_fn){
_fn();
}
foo(); // 最终得到的结果是 2,不是1
复制代码
很明显,这个例子中 执行 foo 最终访问到了 foo 函数作用域下的 a,并拿到了结果 2.
所以,你觉着答案1、2、3 还能完全hold住问题 Q3: 啥是闭包? 好像有点没解释清楚。
那么,当你需要回答这个问题的时候,请回答这两句话。 回答的时候记得要保持十分沉稳的态度!
- 闭包产生的本质当前环境中存在指向父级作用域的引用
- 要产生闭包只需要 在当前环境中使其存在指向父级作用域的引用 至于你用什么手段来保证 2 成立,那是你的本事。再例如:
setTimeout(function(){
console.log('我想签约');
},1000);
// 事件监听
document.addEventListener('click',function(){})
复制代码
实际上,只要是异步回调,基本上都是闭包。
那么,既然讲到了 异步 这个词,那又不得不问:
Q4: 浏览器是咋实现异步操作的?
EventLoop
时间循环,分谁的。 浏览器 Or nodejs。 js 引擎如何处理诸多同步、异步任务的?
浏览器的 Eventloop
容易绕,用顺序来标一下。先看以下 todo list:
需要一个js执行器一行行的去解释你的代码
读到一个作用域就丢进一个 先进后出的 堆栈结构( call stack ,调用堆栈)
好的,当调用堆栈遇到了需要异步处理的某个作用域函数,会把它丢给浏览器的API处理(API独立于JS线程之外)
浏览器API会等待时机将接收到的函数内容交给另一个角色处理( 事件队列)
事件循环 用来控制 事件队列 中的任务,一旦任务空了,则会往里加入新的任务。
是的,这就是个循环,空了加,继续空继续加。 来张图吧,没有图文字有点绕。用来解释这个现象: 事件循环(Eventloop)
再辅以执行代码解读:
代码语言:javascript复制// 全局上下文作用域
const fn1 = () => { console.log('执行fn1')}
setTimeout(fn1,3000)
const fn2 =() =>{
console.log('我想签约')
}
fn2()
const fn3_1 = () => { console.log('执行fn3-1')}
const fn3_2 = () => {console.log('执行fn3-2')}
const fn3 = () => {
fn3_1()
fn3_2()
}
setTimeout(fn3,2000)
复制代码
解释下顺序。
1.call stack 压入全局上下文。等待释放ing。 循环第一轮开始
2.代码解析器读到fn1,创建fn1 的上下文,压入 call stack
3.读到第一个setTimeout,压入call stack, callstack 识别是异步任务,将setTimeout交给 浏览器API去处理。
4.fn2 压入 call stack。
5.读到fn2。fn2是同步任务,属于当前循环,执行完毕。 释放fn2的上下文(V8 执行垃圾回收)
6.同理 生成fn3-1的执行上下文,压入stack
7.fn3-2 压入stack
8.fn3 压入stack
9.setTimeout(fn3,2000) 压入stack,同理交由浏览器API处理。
10.代码执行完毕,判断没有新的任务需要被执行,第一轮循环结束。
11.消息队列被浏览器处理当前要被处理宏任务:setTimeout(fn3,2000), 处理后,fn3-1,fn3-2依次被推入消息队列
12.Even Loop 检测到消息队列有新的微任务,fn3-1 被压入call stack 执行。 执行完毕释放fn3-1.
13.同理执行完毕释放fn3-2.
14.消息队列空了,call stacl没有要执行的任务。当前第二轮循环结束。
15.进入第三轮循环,消息队列被压入了fn1. 直到call stack 执行完fn1. 第三轮循环结束
16.消息队列空了,且无新的任务被压入。
17.最终,call stack 释放全局上下文。
到此三轮循环结束,声明过的对象都被释放掉。
好好理解,做个总结。
一次 Eventloop 循环中,只会处理一个宏任务和本次循环中产生的微任务。
call stack 每次执行任务都会释放掉不存在引用的变量。
所以,针对本个例子,得出结论:
- JavaScript 引擎首先从宏任务队列(macrotask queue)中取出第一个任务;
- 检测微任务(microtask queue)中的任务,有则取出,按照顺序分别全部执
- 执行微任务过程中产生新的微任务,也需要执行,且在当前循环内执行;
- 从宏任务队列中取下一个,重复1、2、3。 当宏任务队列(macrotask queue) 和 微任务(microtask queue)都没有新任务产生的时候,整个循环结束。
源码附件已经打包好上传到百度云了,大家自行下载即可~
链接: https://pan.baidu.com/s/14G-bpVthImHD4eosZUNSFA?pwd=yu27
提取码: yu27
百度云链接不稳定,随时可能会失效,大家抓紧保存哈。
如果百度云链接失效了的话,请留言告诉我,我看到后会及时更新~
开源地址
码云地址:
http://github.crmeb.net/u/defu
Github 地址:
http://github.crmeb.net/u/defu