深入了解JavaScript底层原理
栏目: JavaScript · 发布时间: 6年前
内容简介:变量对象 (缩写为VO)就是与执行上下文相关的对象,它存储下列内容:激活对象是函数上下文里的激活对象AO中的内部对象,它包括下列属性:循环中使用闭包解决 var 定义函数的问题
基本类型: null,undefined,boolean,number(浮点类型),string,symbol(es6)。 对象:Object。 复制代码
类型转换
- typeof:
typeof 1 // 'number' typeof '1' // 'string' typeof undefined // 'undefined' typeof true // 'boolean' typeof Symbol() // 'symbol' typeof b // b 没有声明,但是还会显示 undefined typeof [] // 'object' typeof {} // 'object typeof null // 'object' typeof console.log // 'function' 复制代码
-
valueOf
对象在转换基本类型时,首先会调用 valueOf 然后调用 toString。并且这两个方法你是可以重写的。
let a = { valueOf() { return 0 toString() { return '1'; }, // Symbol.toPrimitive ,该方法在转基本类型时调用优先级最高。 [Symbol.toPrimitive]() { return 2; } } 1 + a // => 3 '1' + a // => '12' 复制代码
- 比较运算符
如果是对象,就通过 toPrimitive 转换对象 如果是字符串,就通过 unicode 字符索引来比较 复制代码
四则运算
只有当加法运算时,其中一方是字符串类型,就会把另一个也转为字符串类型。 其他运算只要其中一方是数字,那么另一方就转为数字。 并且加法运算会触发三种类型转换:将值转换为原始值,转换为数字,转换为字符串。 复制代码
1 + '1' // '11' 2 * '2' // 4 [1, 2] + [2, 1] // '1,22,1' // [1, 2].toString() -> '1,2' // [2, 1].toString() -> '2,1' // '1,2' + '2,1' = '1,22,1' // 对于加号需要注意这个表达式 'a' + + 'b' 'a' + + 'b' // -> "aNaN" // 因为 + 'b' -> NaN 复制代码
冷知识
- NaN 属于 number 类型,并且 NaN 不等于自身。
- undefined 不是保留字,能够在低版本浏览器被赋值 let undefined = 1
2. 实例对象
new
- 在调用 new 的过程中会发生以上四件事情
// 新生成了一个对象 // 链接到原型 // 绑定 this // 返回新对象 function new() { // 创建一个空的对象 let obj = new Object() // 获得构造函数 let Con = [].shift.call(arguments) // 链接到原型 obj.__proto__ = Con.prototype // 绑定 this,执行构造函数 let result = Con.apply(obj, arguments) // 确保 new 出来的是个对象 return typeof result === 'object' ? result : obj } 复制代码
- 执行优先级
function Foo() { return this; } Foo.getName = function () { console.log('1'); }; Foo.prototype.getName = function () { console.log('2'); }; new Foo.getName(); // -> 1 new Foo().getName(); // -> 2 // new Foo() 的优先级大于 new Foo 复制代码
new (Foo.getName()); (new Foo()).getName(); // 对于第一个函数来说,先执行了 Foo.getName() ,所以结果为 1; // 对于后者来说,先执行 new Foo() 产生了一个实例, // 然后通过原型链找到了 Foo 上的 getName 函数,所以结果为 2。 复制代码
this
- 通用规则 new有最高优先级,利用 call,apply,bind 改变 this,优先级仅次于 new。
function foo() { console.log(this.a) } var a = 1 foo() var obj = { a: 2, foo: foo } obj.foo() // 以上两者情况 `this` 只依赖于调用函数前的对象,优先级是第二个情况大于第一个情况 // 以下情况是优先级最高的,`this` 只会绑定在 `c` 上,不会被任何方式修改 `this` 指向 var c = new foo() c.a = 3 console.log(c.a) // 还有种就是利用 call,apply,bind 改变 this,这个优先级仅次于 new 复制代码
- 箭头函数其实是没有 this 的,这个函数中的 this 只取决于他外面的第一个不是箭头函数的函数的 this。在这个例子中,因为调用 a 符合前面代码中的第一个情况,所以 this 是 window。并且 this 一旦绑定了上下文,就不会被任何代码改变。
冷知识
- instanceof 可以正确的判断对象的类型,因为内部机制是通过判断对象的原型链中是不是能找到类型的 prototype。
3. 执行上下文
- 全局执行上下文
- 函数执行上下文
- eval 执行上下文
属性 VO & AO
变量对象 (缩写为VO)就是与执行上下文相关的对象,它存储下列内容:
- 变量 (var, VariableDeclaration);
- 函数声明 (FunctionDeclaration, 缩写为FD);
- 函数的形参
- 只有全局上下文的变量对象允许通过VO的属性名称间接访问(因为在全局上下文里,全局对象自身就是一个VO(稍后会详细介绍)。在其它上下文中是不可能直接访问到VO的,因为变量对象完全是实现机制内部的事情。当我们声明一个变量或一个函数的时候,同时还用变量的名称和值,在VO里创建了一个新的属性。
激活对象是函数上下文里的激活对象AO中的内部对象,它包括下列属性:
- callee — 指向当前函数的引用;
- length —真正传递的参数的个数;
- properties-indexes(字符串类型的整数)
- 属性的值就是函数的参数值(按参数列表从左到右排列)。 properties-indexes内部元素的个数等于arguments.length. properties-indexes 的值和实际传递进来的参数之间是共享的。(译者注:共享与不共享的区别可以对比理解为引用传递与值传递的区别)
属性 this&作用域链
b() // call b console.log(a) // undefined var a = 'Hello world' function b() { console.log('call b') } 复制代码
-
以上众所周知因为函数和变量提升的原因。通常提升的解释是说将声明的代码移动到了顶部。但是更准确的解释应该是:在生成执行上下文时,会有两个阶段。第一个阶段是创建的阶段(具体步骤是创建 VO),JS解释器会找出需要提升的变量和函数,并且给他们提前在内存中开辟好空间,函数的话会将整个函数存入内存中,变量只声明并且赋值为 undefined,所以在第二个阶段,也就是代码执行阶段,我们可以直接提前使用。
-
在提升的过程中,相同的函数会覆盖上一个函数,并且函数优先于变量提升
b() // call b second function b() { console.log('call b fist') } function b() { console.log('call b second') } var b = 'Hello world' 复制代码
- 对于非匿名的立即执行函数需要注意以下一点
var foo = 1 (function foo() { foo = 10 console.log(foo) }()) // -> ƒ foo() { foo = 10 ; console.log(foo) } // 内部独立作用域,不会影响外部的值 复制代码
一个面试题
循环中使用闭包解决 var 定义函数的问题
for ( var i=1; i<=5; i++) { setTimeout( function timer() { console.log( i ); }, i*1000 ); } // 因为 setTimeout 是个异步函数,所有会先把循环全部执行完毕,这时候 i 就是 6 了,所以会输出一堆 6。 复制代码
解决办法
第一种使用闭包
for (var i = 1; i <= 5; i++) { (function(j) { setTimeout(function timer() { console.log(j); }, j * 1000); })(i); } 复制代码
第二种就是使用 setTimeout 的第三个参数
for ( var i=1; i<=5; i++) { setTimeout( function timer(j) { console.log( j ); }, i*1000, i); } // 第三个参数及以后的参数都可以作为func函数的参数,例: function a(x, y) { console.log(x, y) // 2 3 } setTimeout(a, 1000, 2, 3) 复制代码
第三种就是使用 let 定义 i 了
for ( let i=1; i<=5; i++) { setTimeout( function timer() { console.log( i ); }, i*1000 ); } 复制代码
因为对于 let 来说,他会创建一个块级作用域,相当于
{ // 形成块级作用域 let i = 0 { let ii = i setTimeout( function timer() { console.log( i ); }, i*1000 ); } i++ { let ii = i } i++ { let ii = i } ... } 复制代码
4. 深浅拷贝
浅拷贝
- 通过 Object.assign
let a = { age: 1 } let b = Object.assign({}, a) a.age = 2 console.log(b.age) // 1 复制代码
- 通过 展开运算符(…)
let a = { age: 1 } let b = {...a} a.age = 2 console.log(b.age) // 1 复制代码
- 弊端:浅拷贝只解决了第一层的问题。如果接下去的值中还有对象的话,那么就又回到刚开始的话题了,两者享有相同的引用。要解决这个问题,我们需要引入深拷贝。
深拷贝
- 通过 JSON.parse(JSON.stringify(object))
let a = { age: 1, jobs: { first: 'FE' } } let b = JSON.parse(JSON.stringify(a)) a.jobs.first = 'native' console.log(b.jobs.first) // FE 复制代码
该方法也是有局限性的:会忽略 undefined,忽略函数,不能解决循环引用的对象
let obj = { a: 1, b: { c: 2, d: 3, }, } obj.c = obj.b obj.e = obj.a obj.b.c = obj.c obj.b.d = obj.b obj.b.e = obj.b.c let newObj = JSON.parse(JSON.stringify(obj)) // 会报错 console.log(newObj) 复制代码
- 如果你的数据中含有以上三种情况下,通过 lodash 的深拷贝函数,或者使用 MessageChannel
function structuralClone(obj) { return new Promise(resolve => { const {port1, port2} = new MessageChannel(); port2.onmessage = ev => resolve(ev.data); port1.postMessage(obj); }); } var obj = {a: 1, b: { c: b }} // 注意该方法是异步的 // 可以处理 undefined 和循环引用对象 const clone = await structuralClone(obj); 复制代码
文章为学习笔记,整理自面谱InterviewMap。复制代码
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:- 深入理解 MySQL 索引底层原理
- 深入解读JSP的底层解析过程
- 深入底层,仿MyBatis自己写框架
- 深入iOS系统底层之静态库介绍
- 深入理解 MySQL 索引底层数据结构
- 深入理解 MySQL 索引底层数据结构
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。