写在前面
如何打好JavaScript基础?深入剖析V8的代码执行机制--JS基础篇(一)
在学习作用域和闭包时,我从JS的编译角度体会了代码在V8引擎中执行的艺术。我认为这是很有必要的,作用域和闭包的知识可以让我从整体上把握读懂一份代码的条件,从原本的只会看,到现在学会自己梳理代码的编译和执行过程,确实升华了很多。
👀学习了JS一段时间,我感觉它好像一位蒙着面纱的姑娘,每个角度看去,都有让我不得其解得的地方。不过,越是这样我越好奇。这次,我将从随处可见的变量开始,揭开JS的面纱。
这次从变量讲起,涉及到前面几篇的知识也会说明。
变量的引入
变量就是JS里面的一个容器,用于存放各种类型的数据。
数据类型(7种)
- Number类型:表示整数和浮点数,其中浮点数最大值:Number.MAX_VALUE,最小值:Number.MIN_VALUE。
var num = 35;
var num1 = 3.33;
Number类型上其他相关的静态数据属性:
Number.MAX_SAFE_INTEGER 最大的安全整数(2^53 - 1)
Number.MIN_SAFE_INTEGER 最小的安全整数(-2^53 - 1)
“安全”指的是能够精确表示整数并正确比较它们
- String类型:表示字符序列,通过字符串字面量方式创建原始值或String() 方式创建字符串对象。
var str = 'I love JavaScript!';
var str1 = new String('Me too.');
- BigInt类型:一个数字的原始值,表示大于
2^53-1的整数,可以通过在一个整数字面量后面加n的方式创建或调用函数BigInt() 创建。
var bigNum = 8888888888888888888n;
var bigNum1 = BigInt(99999999999999999999);
- Boolean类型:表示一个逻辑实体,值为true或false。
var bool = true;
var bool1 = false;
- undefined类型:表示值的缺失,只定义了,但没有赋值,唯一值为undefined,一个没有被赋值的变量类型为undefined。
var un = undefined;
- Null类型:唯一值为null,表示变量未指向任何对象。
var nu = null;
- Symbol类型(原子类型):唯一且不可变的原始值,为对象创建一个唯一的属性键,保证不会与其他键冲突。
const symbol = Symbol(1);
const symbol1 = Symbol(2);
console.log(symbol === 1);// false
console.log(symbol === symbol1);// false
console.log(Symbol(1) === Symbol(1));//false
console.log(Symbol(1) == Symbol(1));//false
变量的引入,赋予了JS这门编程语言生命。变量是代码的运行基础,因为没有变量存储数据,再华丽的代码也只是无用的工具。
那么,如何定义变量(选择哪个关键字),在哪里定义变量就应当十分考究了!
定义变量
JS目前提供了三个定义变量的关键词:var、let和*const。从变量状态来看,const定义的变量状态为不可修改(在大部分情况下),也叫做常量,变量名用大写表示。我们先单独看看。
var关键字
var是三者当中官方最早给出的一个,用于在全局作用域或函数作用域内声明变量的关键字,其声明的变量默认值为undefined。
-
var变量的声明提升:
console.log(a);//undefined var a = 2;等价于:
//注意这样写是错误的,这里只是为了演示 var a; console.log(a); a = 2;此处打印的结果就是undefined,因为var a在
编译阶段被识别,而在执行阶段,先执行第三行输出a,再为a赋值2。事实上,在编译阶段,函数也具有声明提升的过程,且先于变量的声明提升。
-
var变量的重新声明:
作为JS这门语言中的"元老",V8编译器对var十分纵容,即使是在
严格模式下,使用var重新声明同名的变量也不会报错,这点与let、const十分不同。function testVar() { var x = 5; if (true) { var x = 10; // 这里的x会覆盖外层的x console.log(x); // 10 } console.log(x); // 10,因为var变量提升了且在函数内可被覆盖 }这个特性似乎一直被人诟病,在复杂的代码块之中,如果开发者粗心地使用var定义(声明并且赋值)一个已经存在的同名变量,并且将其绑定到了某个业务逻辑中,那么将导致未知的错误且难以溯源。
let关键字
let在ES6中引出,用于声明块作用域的局部变量,其声明的变量默认值为undefined。
-
块级作用域
当let(也包括const) 与一对
{}结合时,let所在行就会构成一个块级作用域,其声明的变量无法被外部作用域访问。function testLet() { let y = 5; if (true) { let y = 10; // 这是另一个y,仅在if块内有效 console.log(y); // 10 } console.log(y); // 5,因为if中的let形成了块级作用域 } -
暂时性死区(Temporary dead zone)
引入暂时性死区是为了说明let(其实也包括const)声明变量时,V8引擎不会对其执行声明提升,而是将其放进暂时性死区 ,待执行到所在行时才允许访问它们。
console.log(a);//ReferenceError: Cannot access 'a' before initialization. let a = 2;当console.log()企图访问a变量时,由于a在暂时性死区中,还未被初始化,禁止被访问,故报错。
我们再来看一个经典的例子,源自MDN:
function test() { var foo = 33; if (foo) { let foo = foo + 55; // ReferenceError } } test();大家有
get到点子吗?准确地说。在调用函数test()时,"=" 所进行的赋值操作(第4行)势必要先访问foo的引用,但是此时foo仍然在暂时性死区当中,直到这句代码执行结束才会离开。在使用let、const声明变量时初始化的值不应该包含变量自身。
const关键字
与let一起在ES6中引入,用于声明块作用域内的局部变量,定义的变量值不能被二次修改,对象除外,无默认值。
-
声明常量
本着追求新技术的准则,我们要学会运用JS中的那些新特性,这些特性大都是原有语法漏洞的补充,能在开发中堪当大用。将原本只能用var声明的常量改为const声明,在一定程度上保证了安全。
const PROJECT_NAME = 'Dream'; const PI = 3.1415926;这样,即使是人为地对常量重新赋值,也会被禁止,不过这一特性似乎在对象上失效了。
const obj = { name:'xiaoming', age:18, hobby:'programming' } obj.age = 20; console.log(obj);//{ name: 'xiaoming', age: 20, hobby: 'programming' }上面我们使用const定义了一个对象,const 定义的对象其属性默认是可以修改的,这点对数组对象同样适用。我们还。可以通过
Object.freeze()来冻结一个对象,保证其不能被二次修改,细节在本文后半部分我会详细梳理。 -
初始化要求 与
var、let不同的是,当我们使用const声明一个常量后 必须 要对其赋值,V8引擎不会给予其默认值undefined,而是直接抛出一个错误。
那些你不知道细节
总讲定义,好像太繁杂,那我们就来看看那些鲜为人知的细节:
-
面试官:如何用var定义一个常量?
如果在面试的时候遇到这个问题,你会不会怀疑自己的听力呢?var还能定义一个常量?
没错!我们要知道,在ES6发布(2015年)之前,是没有const关键字的,那个时候的项目中如何去创建一个常量呢?这么一想,好像还真有了解的必要,毕竟以后去到一个公司,总有些代码是按照以前的规范写的,我们要想读懂就得知道这些细节。
var PI = 3.1415926;事实是,通常我们用var去定义一个常量时,会将常量名大写,这是一种编程习惯,开发者遇见大写的常量,自然不会去修改,不过目前使用const定义显然是一种更合理的方式。
但是对于常量对象,官方为我们提供了一些方法去修改对象的可修改性,让其变得不可修改。
var obj = { name:'xiaoming', age:20 } console.log(obj.age);//20 Object.defineProperty(obj,age,{ value:19,//此处可以修改值 writable:false,//禁止修改 }) obj.age = 18; console.log(obj.age);//19Object.defineProperty()能够为对象定义一个属性(如果属性未定义过),并提供了修改可见性的配置writable:false;。等价于:
var obj = { name:'xiaoming', age:20 } Object.freeze(obj); obj.age = 18; console.log(obj.age);//20Object.freeze()表示冻结一个对象,使其不能被修改。对于const定义的对象,如果需要,我们也可以 "冻结" 它。
面试题
-
面试官:这题结果是什么?
var arr = [] for(var i = 0 ; i < 10 ; i++){ arr[i] = (function(){ return console.log(i); }) } arr.forEach(function(item){ item(); })这里使用一个for循环为arr数组赋值,但是值得注意的是,返回的
console.log(i)语句并没有立即执行,而是在for循环执行完之后再通过数组的forEach()方法调用打印。我们应当知道,var上声明的变量i的词法作用域是全局作用域,也就是说forEach()访问的是全局下的i,for循环结束之后i的值为10,故结果是打印10个10。item类型为函数,执行到item()这句代码时,就会调用arr数组中函数,打印i。
-
面试官:那如何修改,让结果为0-9?
我们仔细观察会发现,因为var声明的变量i会有一个声明提升的过程,导致打印的全是同一个在全局作用域中的变量i。那么,我们只需要让其打印的分别是10个不同的变量i就成功了。
var arr = [] for(let i = 0 ; i < 10 ; i++){// 使用let声明,每个i都在单独的词法作用域 arr[i] = (function(){ return console.log(i); }) } arr.forEach(function(item){ item(); })这里将
var改为let就解决了问题,这是因为let与{}形成了一个块级作用域,每次循环产生的变量i都在一个单独的块级作用域,彼此独立。面试官听了你的解释,点了点头,意味深长地继续问道:不使用let,还有别的方法吗?
你:emmmm,能给点提示吗?
面试官:你上面使用了块级作用域的特性,再想想有没有类似的方式。
你瞬间想到了函数作用域,但是要求不使用let,意味着还得使用var定义变量,好像只有...闭包,将数组赋值过程放在一个函数作用域中,每次
for循环就结束一次函数的调用。具体是这样:var arr = [] for(var i = 0 ; i < 10 ; i++){// 使用let声明,每个i都在单独的词法作用域 (function(j){ arr[j] = (function(){ console.log(j); }) })(i) } arr.forEach(function(item){ item(); })在这里我们使用立即执行函数(IIFE) 给数组赋值,这里巧妙地运用了闭包的特性。在for循环中每次都
i传给j执行一次立即执行函数,但是其内还有一个内部函数被赋给了数组arr,当立即执行函数调用结束后,内部函数对j的引用仍然在闭包中。即使外部函数(立即执行函数)调用结束,使用forEach()时也能准确地拿到每一个闭包里的变量j。😊至此,你完美地回答出了这一题。面试官面带微笑,继续给出了下一个让你头疼的难题。
总结
本文梳理了:
- 基本数据类型
- var、let和const的特性
- var和let结合作用域、闭包在面试题中的考点
写文不易,如果你觉得对你有帮助,那么点个小赞,这将是我继续创作的动力,感谢!
参考:
MDN Web Docs (mozilla.org)