JavaScript 核心原理剖析

2022-07-22 17:37:54 浏览数 (1)

写在最前

本文针对2类人群。

  1. 面试总挂在js基础上的人
  2. 想夯实js基础 我这也算一个JS基础的体系了。 既然是体系肯定比盲目去网上捞资源来得靠谱,放心入坑。

了解一门语言先从数据类型开始

Q1: js有哪些数据类型? 6种还是8种?

答: 直接上张图你就明白了。

QQ截图20220722172352.pngQQ截图20220722172352.png

喏,你是不是回答漏了其中一两个?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
复制代码

打开你的控制台,输入以下,你会得到:

QQ截图20220722172504.pngQQ截图20220722172504.png

所以得到结论: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】 一下,会经历:

  1. 创建一个新对象(Object);
  2. 将构造函数的作用域赋给新对象(也就是 this 要指向新对象);
  3. 执行构造函数中的代码(给对象挂上属性);
  4. 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: 啥是闭包? 好像有点没解释清楚。

那么,当你需要回答这个问题的时候,请回答这两句话。 回答的时候记得要保持十分沉稳的态度!

  1. 闭包产生的本质当前环境中存在指向父级作用域的引用
  2. 要产生闭包只需要 在当前环境中使其存在指向父级作用域的引用 至于你用什么手段来保证 2 成立,那是你的本事。再例如:
代码语言:javascript复制
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)

QQ截图20220722173526.pngQQ截图20220722173526.png

再辅以执行代码解读:

代码语言: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 释放全局上下文。

到此三轮循环结束,声明过的对象都被释放掉。

QQ截图20220722173718.pngQQ截图20220722173718.png

好好理解,做个总结。

一次 Eventloop 循环中,只会处理一个宏任务和本次循环中产生的微任务。

call stack 每次执行任务都会释放掉不存在引用的变量。

所以,针对本个例子,得出结论:

  1. JavaScript 引擎首先从宏任务队列(macrotask queue)中取出第一个任务;
  2. 检测微任务(microtask queue)中的任务,有则取出,按照顺序分别全部执
  3. 执行微任务过程中产生新的微任务,也需要执行,且在当前循环内执行;
  4. 从宏任务队列中取下一个,重复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

0 人点赞