写在最前
本文针对2类人群。
1. 面试总挂在js基础上的人
2. 想夯实js基础
我这也算一个JS基础的体系了。 既然是体系肯定比盲目去网上捞资源来得靠谱,放心入坑。
了解一门语言先从数据类型开始
Q1: js有哪些数据类型?
6种还是8种?
答: 直接上张图你就明白了。
喏,你是不是回答漏了其中一两个?7 种基础类型, 1 种引用类型。请注意以下两点:
- 基础类型存储在栈内存。
被引用或拷贝时,会创建一个完全相等的变量
; - 引用类型存储在堆内存。
存储的是地址,多个引用指向同一个地址。
Q2: 你知道哪些判断数据类型的方法?
typeof
看代码:
typeof null // 'object'
typeof 1 // 'number'
typeof '1' // 'string'
typeof undefined // 'undefined'
typeof true // 'boolean'
typeof Symbol() // 'symbol'
噢吼,虽然知道typeof 可以用来判断基本数据类型,但为啥 typeof null === 'object'
?
再看代码:
Object.prototype.__proto__
Object.prototype.__proto__.a
打开你的控制台,输入以下,你会得到:
所以得到结论:null 可以是顶层对象Object的上层描述,但null也是‘objcet’,因为null在Object的链上。 所以判断变量是否为null,请直接 使用 xxx === null
判断。
instanceof
好吧,继续看代码
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
。 我给你上代码了:
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]’
所以,我相信你肯定看明白了。 总结成一个方法就是:
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()
Number(true); // 1
Number(false); // 0
Number('00010086'); // 10086
Number(null); // 0
Number(''); // 0
Number('1a'); // NaN
Number('0X11') // 34
Number(-0X22); // -34
我相信你又懂了。 读书百遍不如自己亲自验证一下?
Boolean()
Boolean({}) // true
Boolean(0) // false
Boolean(null) // false
Boolean(undefined) // false
Boolean(NaN) // false
Boolean(10086) // true
Boolean('12') // true
+ 号 隐式转换
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 + {} = '1[object Object]'
对吗? 控制台试试?
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'
好吧。 这个解释不能再精简了,还是得写写。
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】
一下,会经历:
1. 创建一个新对象(Object);
2. 将构造函数的作用域赋给新对象(也就是 this 要指向新对象)
;
3. 执行构造函数中的代码(给对象挂上属性);
4. return newObject。
但实际上这个过程背地里要做以下三件事情。
- 让new出来的实例拥有访问私有成员属性的权限
- 实例可以访问构造函数原型所在原型链上的属性(也就是 constructor.prototype 上的属性);、
- 构造函数返回的对象必须是引用数据类型
上个代码,其实不想上,网上答案一大堆。 但是还是希望有空自己手写一下……
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
先给个提醒,大家来找茬。
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
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 一个函数
所以,你觉得这两种答案对吗? 别着急回,因为可能回光速打脸。
看代码:
var f1;
function f2() {
var f1 = 2
f2 = function() {
console.log(f1);
}
}
f2();
这个看起来有点怪? 为什么 能在最外层直接访问 f2
? 然后好像f2
内部又访问了 f1
? 这个 f1
是哪个f1
?
想不清楚就再来看个例子:
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: 啥是闭包?
好像有点没解释清楚。
那么,当你需要回答这个问题的时候,请回答这两句话。 回答的时候记得要保持十分沉稳的态度!
1. 闭包产生的本质当前环境中存在指向父级作用域的引用
2. 要产生闭包只需要 在当前环境中使其存在指向父级作用域的引用
至于你用什么手段来保证 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)
再辅以执行代码解读:
// 全局上下文作用域
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)
解释下顺序。
- call stack 压入全局上下文。等待释放ing。 循环第一轮开始
- 代码解析器读到fn1,创建fn1 的上下文,压入 call stack
- 读到第一个setTimeout,压入call stack, callstack 识别是异步任务,将setTimeout交给 浏览器API去处理。
- fn2 压入 call stack。
- 读到fn2。fn2是同步任务,属于当前循环,执行完毕。 释放fn2的上下文(V8 执行垃圾回收)
- 同理 生成fn3-1的执行上下文,压入stack
- fn3-2 压入stack
- fn3 压入stack
- setTimeout(fn3,2000) 压入stack,同理交由浏览器API处理。
- 代码执行完毕,判断没有新的任务需要被执行,第一轮循环结束。
- 消息队列被浏览器处理当前要被处理宏任务:setTimeout(fn3,2000), 处理后,fn3-1,fn3-2依次被推入消息队列
- Even Loop 检测到消息队列有新的微任务,fn3-1 被压入call stack 执行。 执行完毕释放fn3-1.
- 同理执行完毕释放fn3-2.
- 消息队列空了,call stacl没有要执行的任务。当前第二轮循环结束。
- 进入第三轮循环,消息队列被压入了fn1. 直到call stack 执行完fn1. 第三轮循环结束
- 消息队列空了,且无新的任务被压入。
- 最终,call stack 释放全局上下文。
到此三轮循环结束,声明过的对象都被释放掉。
好好理解,做个总结。
- 一次 EventLoop 循环中,只会处理一个宏任务和本次循环中产生的微任务。
- call stack 每次执行任务都会释放掉不存在引用的变量。
所以,针对本个例子,得出结论:
1. JavaScript 引擎首先从宏任务队列(macrotask queue)中取出第一个任务;
2. 检测微任务(microtask queue)中的任务,有则取出,按照顺序分别全部执
3. 执行微任务过程中产生新的微任务,也需要执行,且在当前循环内执行;
4. 从宏任务队列中取下一个,重复1、2、3。 当宏任务队列(macrotask queue) 和 微任务(microtask queue)都没有新任务产生的时候,整个循环结束。
写文不易,手动🐶 下篇文开始就会涉及 异步编程以及nodejs Event Loop。 下期见~
( 我正在参与掘金技术社区创作者签约计划招募活动,点击链接报名投稿。 )