JavaScript这些基础可以不知道??(持续更新~~)

it2025-05-04  18

01. JS加载阻塞 02. 变量提升 03. TDZ(暂存性死区) 04. 块级作用域以及全局污染 05. 严格模式"use strict" 06. 类型检测 07. 隐式转换 08. 正式学习一下Symbol 09. 非常有趣的Set 10. 可以解决不能用对象作键的问题Map 11. 深究函数 12. 任务管理

精通JavaScript

1. JS加载阻塞

一般地,一个包含外部样式表和外部脚本问价的HTML载入和渲染过程是这样的:

浏览器下载HTML文件并开始解析DOM;遇到样式表文件时,将其加入资源文件下载列表,继续解析DOM;遇到脚本文件时,暂停DOM解析并立即下载脚本文件脚本文件下载好后立即执行,此时脚本只能访问当前已加载的DOM元素;脚本执行结束,继续解析DOM;DOM解析完成,出发DOMContentLoaded事件。

所以阻塞就是DOM还没有解析渲染完毕,由于JS文件的优先下载解析导致页面不能正常显示一段时间,也就是白屏

解决办法:

推迟加载(延迟加载)

初始页面如果不依赖与js就可以用推迟加载,将js文件在body的最后引入。

defer延迟加载

使用

异步加载

使用,它会异步加载JS不会导致页面阻塞,或者动态创建script标签

<script type="text/javascript"> (function() { let s = document.createElement('script'); s.type = 'text/javascript'; s.sync = true; s.src = 'xxx.js'; let x = document.getElementByTarName('script')[0]; x.parentNode.insertBefore(s,x); })(); </script>

2. 变量提升

解析器会在执行代码之前,先解析一遍,在解析的过程中如果有变量的使用在变量的定义之前,那么解析器就会做一步操作,提升变量

1. var声明的变量提升

// 我们写的代码 console.log(a); // undefined var a = 1; console.log(a); // 1 // 真正执行的代码(解析器修改后的代码) var a; console.log(a); a = 1; console.log(a);

2. if(false)的变量提升

// 我们写的代码 var user = 'zs'; (function() { if(false) var user = 'ls'; console.log(user); // undefined })(); // 解析器处理过后 var user = 'zs'; (function() { var user; if(false) user = 'ls'; console.log(user); })

3. TDZ(暂存性死区)

​ var定义的变量输出在定义之前会输出undefined,也就是变量提升;但是let/const声明的变量在声明前存在暂存性死区,会报错,也就是说,我们使用let/const定义变量必须先声明后使用

1. 声明变量时的TDZ

console.log(x); // Cannot access 'x' before initialization let x = 1;

2. 函数中的TDZ

(function work() { console.log(user); // 报错 let user = 'zs'; })();

3. 函数参数的TDZ

(function work(a=b, b=3) {})(); //Connot access 'b' before initialization

也就是说,在b还不知道是什么的情况下,是无法赋值给a的

4. 块级作用域以及全局污染

let/const/var他们的共同点是全局作用域中声明的变量,在函数中都能使用

// var/let 都一样 let a = 3; function fun() { a = 5; console.log(a); // 5; } fun(); console.log(a); // 5

个人理解是: 在局部使用到一个变量是会先在自己的局部范围内找,如果没有该变量,则再向上一级寻找

由于var没有块级作用域,它声明的变量不限制于自己的局部,造成的全局污染

块级作用域个人理解:在一对{}存在,但出了这里,啥也不是,没人知道,也没人认识

var i = 2; for (var i=0;i<10;i++) {}; console.log(i); // 10;

显然这不是我们想要的结果,我们不希望for循环里面的i影响到我们全局变量i;所以我们就使用有块级作用域的let来定义变量,就不会造成这样的污染

let i = 2; for (let i=0;i<10;i++) {}; console.log(i); // 2;

想一想我们所谓的全局不就是window对象嘛,它会不会也受到污染

// 先来看结果 var lsj = 'you'; console.log(window.lsj); // you;

果然会污染,那当我们定义的变量刚好和window中的变量重名,那岂不是出事了

var screenLeft = 500; console.log(window.screenLeft); // 500

window.screenLeft自动获取窗口距离屏幕左边的距离,由于var的全局污染,导致它是一个定值了,所以我们应该避免使用var

5. 严格模式"use strict"

虽然我们了解到了let的好处和var的坏处,但是var依然是可以声明变量的,这时候使用严格模式就可以避免很多问题

1. 强制声明防止污染全局之不声明就报错

"use strict"; lsj = 'you'; console.log(lsj); // lsj is not defined;

2. 关键词不允许作为变量,用就报错

"use strict"; var public = "xxx"; // Unexpected strict mode reserved word

3. 参数不允许使用重复参数名,用就报错

"user strict"; function xxx(name, name) {}; //Duplicate parameter name not allowed in this context

当我们给某个函数使用"use strict"时,这个要求会向下传递,也就是说,此函数内部所有内容都使用严格模式,而此函数之外不作要求

6. 类型检测

类型检测的方法挺多的,我们这之说三种typeof,instanceof,Object.prototype.toString(),首先我们应该清楚他们的使用格式 typeof,instanceof是一个操作符,并不是方法,Object.prototype.toString()是一个方法,所以用法有点区别

1. typeof检测基本数据类型

typeof xx; // 使用方法;返回的是一个字符串类型的xx的数据类型 console.log(typeof '123'); // string; 注意返回的其实是"string"; // 所以这个问题我们就需要注意一下啦 console.log(typeof typeof 1); // string;凡是只要大于等于两次嵌套使用typeof的输出都是string; 代码结果typeof 1;numbertypeof 'str';stringtypeof true;booleantypeof NaN;numbertypeof undefined;undefinedtypeof null;objecttypeof new Object();objecttypeof new Array();objecttypeof function() {};object

很明显typeof只能判断出基本数据类型(null除外)

2. instanceof:基于原型链的判断

原型和原型链也是JS的一个重点哦(不过先不慌)

[] instanceof Array; // []基于Array类型? 返回值为 true 操作结果let a = []; a instanceof Array;truelet a = {};a instanceof Object;truelet a =()=>{};a instanceof Function;truelet a = new Date(); a instanceof Date;truelet a = /w/g; a instanceof RegExp;ture

这就完了?就这?当然不会,还有一个烦人的事就是,以上的五种类型instanceof Object都是true,就问你怕不怕??这个要从原型说起了,

不着急,我们目前只要大概清楚这些的原型上都有Object就行了,好像也说得通,不愧是我

3. Object.prototypeo.toString()高级的方法

这里会涉及call(),apply()两个方法,它们两个除了传入的第二个参数有点区别没有其他区别

let a = 1; Object.prototype.toString.call(a); // [object Number]; a = {}; Object.prototype.toString.apply(a); // [object Object]; 输入输出let a = 1;[object Number]let a = 'str';[object String]let a = Symbol();[object Symbol]let a = function(){};[object Function]

可以看出不管输入原始数据类型还是引用类型,Object.prototype.toString()都可以精准的返回它的类型

7. 隐式转换

基本上所有类型都可以转为Boolean类型

数据类型truefalseString非空字符串''Number非0数值0Array数组不参与比较时候比较时的空数组Object所有对象undefined无undefinednull无nullNaN无NaN console.log(3== true); // false; en?不是说好非空字符串转为true嘛?

往往在我们进行 == 比较的时候,两边的类型都要转变为Number类型,而在判断时需要转变为Boolean.

### 8. 正式学习一下`Symbol`

Symbol用于解决属性名冲突而产生的,Symbol值是唯一的,独一无二不会重复的

1. symbol独一无二

let zs = Symbol(); let ls = Symbol(); console.log(zs == ls); // false;

2. Symbol不可以添加属性

let zs = Symbol(); zs.name = 'xxx'; console.log(zs.name); // undefined;

3. 给Symbol添加一个描述

let zs = Symbol('我是张三'); let ls = Symbol('李四'); console.log(zs); // Symbol(我是张三); console.log(ls); // Symbol(李四);

即使传入相同的描述它依旧是独一无二的,就跟现实生活中两个仅仅名字相同的人的关系一样

4. Symbol.for注册一个"名字"

let zs = Symbol.for('张三'); let ls = Symbol.for('张三'); console.log(zs == ls); // true;感觉智商在被调戏,说好的独一无二呢?

Symbol.for('xx')将xx这个描述注册了,这样以后用它描述的Symbol.for只能是我,想想其实和现实挺像的,但是不能在用名字来作比较了,身份证可以吧

5. Symbol.keyFor,询问注册描述(身份证号)

let zs = Symbol.for('张三'); console.log(Symbol.keyFor(zs)); // 张三; let ls = Symbol('李四'); console.log(Symbol.keyFor(ls)); // undefined;

6. 给对象设置Symbol属性

Symbol出现的初衷就是解决属性名重复问题

Symbol声明和访问使用[]操作;不能使用 . 操作取值 let zsName = Symbol('张三'); let obj = { [zsName]: 'zhangsan' }; console.log(obj[zsName]); // zhangsan;

7. Symbol保护机制

for/in for/of都不能遍历对象中的 Symbol 属性,但是使用Object.getOwnPropertySymbols() 可以获取所有Symbol属性,使用 Reflect.ownKeys() 可以获取所有属性

let name = Symbol('xxx'); let obj = { [name]: 'you name', // 把名字保护起来 age: 18 } // for/in 遍历 for (let key in obj) { console.log(key); // age; 不会输出name; } // Object.getOwnPropertySymbols()获取所有Symbol属性 for(const key of Object.getOwnPropertySymbols(obj)) { console.log(key); // Symbol(xxx) } // Reflect.ownKeys()获取所有属性 for(const key of Reflect.ownKeys(obj)) { console.log(key); // age,Symbol(xxx); }

9. 非常有趣的Set

用于储存任何类型的唯一值

只能保存值没有键名严格类型检测(1 和 '1’是不同的)值是唯一的遍历顺序是添加顺序,方便保存回调函数

1. 基本用法

let set = new Set(); // 初始化一个字典,可以传入初始数据(数组或字符串形式) set.add(1); // 添加一个内容 set.size; // 获取字典内容数量 set.has(1); // 返回true,判断是否存在检测值 set.delete(1); // 删除一个内容 set.clear(); // 清空

2. 转换为数组

let set = new Set('12345'); // Set {'1','2','3','4','5'} let set2Arr = [...set]; // ['1','2','3','4','5']

3. 遍历数据

使用keys()/values()/entried()都可以返回可迭代对象,因为Set只有value,所以返回的value,key是一样的

const test = new Set([1,2,3,4]); console.log(test.values()); // [Set Iterator] { 1, 2, 3, 4 } console.log(test.keys()); // [Set Iterator] { 1, 2, 3, 4 } console.log(test.entries()); // [Set Iterator] { 1, 2, 3, 4 }

也可以使用forEach for/of遍历

// forEach let test = new Set([1,2,3,4]); test.forEach((item,key)=>{console.log(item,key)}); // for/of for (const item of test) { console.log(item); }

4. 只能存放对象类型的WeakSet

WeakSet 结构同样不会存储重复的值,而且它的值只能是对象类型

垃圾回收不考虑 WeakSet, 即被WakSet 引用是引用计数器不加一,所以对象不被引用是不管WeakSet是否在使用都将删除因为WeakSet是弱引用,由于其他地方操作成员可能不会存在,所以不可以进forEach遍历等操作因为是弱引用,WeakSet结构没有keys(),values(),entried()等方法和size属性因为是弱引用,所以当外部引用删除时,希望自动删除数据使用WeakMap const test = new WeakSet(); const child = [1,1]; test.add(arr); // 添加一个数据 test.delete(arr); // 删除一个数据 test.has(arr); // false,检索判断

5. WeakSet的垃圾回收

WeakSet保存的对象不会增加引用计数器,如果一个对象不被引用就会自动删除

下例中的数组被引用,计数器+1数据有添加到了test的WeakSet中,引用计数还是1当数组设置为null是,引用计数-1此时对象引用为0当垃圾回收是对象被删除,这是WeakSet也就没有记录了 const test = new WeakSet(); let arr = ["ls"]; // 被引用,计数器+1 test.add(arr); // 数据添加到了test的WeakSet中,引用计数还是1 arr = null; // 将数组设置为null console.log(test); // WeakSet { [items unknown] } setTimeout(()=>{ console.log(test); // WeakSet { [items unknown] } })

10. 可以解决不能用对象作键的问题Map

Map 是一组键值对的结构,用于解决以往不能用对象作为键的问题

具有极快的查找速度函数、对象、基本类型都可以作为键或值

1. 声明定义

可以接受一个数组作为参数,该数组的成员是一个表示键值对的数组

let m = new Map([ ['zs','张三'], ['ls','李四'] ]); console.log(m.get('zs')); // 张三

使用set方法添加元素,注意哦是set,之前的是add,支持链式操作

let map = new Map(); let obj = { name: '张三' }; map.set(obj,'zs').set('name','ls'); // set的链式操作添加两个元素 console.log(map.entries()) // [Map Iterator] { [ { name: '张三' }, 'zs' ], [ 'name', 'ls' ] }

11. 深究函数

函数就是将复用的代码封装起来

1. 声明定义

在JS中函数也是对象函数是Function类创建的实例,但是标准语法是使用函数声明来定义函数

// 不常用的Function实例创建函数 let fun = new Function("title","console.log(title)"); fun('zs'); // zs // 标准语法 function func(title) { console.log(title); }; func('ls'); // ls

在对象中,如果我们要书写一个函数属性的话,可以使用它的简写形式

let user = { name: 'zs', getName: function (name) { return this.name; }, // 简写 setName(value) { this.name = value; } } user.setName('ls'); console.log(user.getName()); // ls

我们定义的所有函数都是压入window对象中,也就是说全局的外层就是window对象,window中本来存在一些自己的函数,如果我们写的函数名刚好和window对象中的相同,那么原本window中的方法就会无法被正常调用,但是用let/const声明的函数不会压入window对象中

function test() { console.log('hhh'); }; window.test(); // hhh let letTest = function() { console.log('let声明'); } window.letTest(); // window.letTest is not a function

2. 匿名函数

匿名函数就是指函数本身没有名字,只是将这个函数赋值给了一个变量

let test = function(num) { return ++num; }; console.log(hd instanceof Object); // true let newTest = test; console.log(newTest(3)); // 4

小知识点:如果let newTest = test();的话,结果就不一样了哦,不带括号相当于将整个函数赋值给新变量,如果加括号的话就相当于把函数的返回值赋值给新变量

标准声明的优先级高于赋值声明

console.log(test(3)); // 4 // 标准声明 function test(num) { return ++num; } // 赋值声明 var test = function(num) { return --num; };

而且有同名的标准声明函数之后,不能够在使用let/const赋值式声明,只能用var

3. 立即执行函数(函数被定义时立即执行)

可以用来定义私有作用域防止污染全局作用域(前面说过了全局污染哦) "use strict"; (function () { var name = 'zs'; })(); console.log(web); //web is not defined let/const有块级作用域,所以只需要一对{}就可以将变量放在私有作用域 { let name = 'ls'; } console.log(name); // name is not defined

4. 函数提升

函数提升 ~= 变量提升(还记得吗??),而且函数提升优先级大于变量提升,但是赋值函数不存在函数提升

// 变量提升 console.log(name); // undefined var name = 'zs'; // 函数提升 console.log(test(2)); // 3 function test(num) { return ++num; } // 赋值函数不会提升 var test = function (num) { console.log(--num); };

实参,形参,默认参数,可选参数那些都挺好理解的,尤其是ES6给出的方法,真贴心

5. arguments是什么?

arguments是函数获得的所有参数的集合,配合ES6的展开语法

function sum(...args) { return args.reduce((a,b) => { return a + b; }) } console.log(sum(1,2,3,4)); // 10 // 小知识点,箭头函数在只有一个参数时()可以省略,只有一条return时大括号可以省略,所以第二三行可以改写 return args.reduce((a,b)=>a+b);

12. JS任务管理

JavaScript是单线程,也就是说同一个时间只能处理一个任务,为了协调时间、用户交互、脚本、UI渲染和网络等行为,防止主线程阻塞,Event Loop的方案就诞生了

JavaScript 处理任务是在等待、执行、休眠等待中不断循环(Event Loop)

主线程任务全部完成,才开始执行队列任务中的任务有新的任务就加入任务队列,采用先进先执行策略

任务包括script(整体代码) setTimeout setInterval DOM渲染 DOM事件 Promise XMLHTTPRequest等

理解一下宏任务和微任务

宏任务包括同步宏任务和异步宏任务,没必要太死磕概念

console.log('同步宏任务,代号001'); setTimeout(function() { console.log("异步宏任务")}, 0); new Promise(resolve =>{ console.log("Promise是同步宏任务,代号002")}) // 注意了Promise本体是一个同步宏任务 .then(function() { console.log(".then是微观任务01"); resolve(); }) .then(function() { console.log(".then是微观任务02"); }); console.log("同步宏任务,代号003"); 单线程,先走同步宏任务,见到异步任务先放入事件队列(.then是异步微任务)同步任务执行完毕,再去遍历事件队列的微任务微任务做完后开始顺序执行异步任务

总结一下也就是:同步 -> 微任务 -> 异步任务

// 输出 同步宏任务,代号001 Promise是同步宏任务,代号002 同步宏任务,代号003 .then是微观任务01 .then是微观任务02 异步宏任务

输出问题

以下代码输出结果

setTimeout (() => { console.log("定时器"); setTimeout(() => { console.log("定时器2"); },0); new Promise(resolve => { console.log("Promise in timeout"); resolve(); }).then(() => { console.log("then in timeout"); }); },0); new Promise(resolve => { console.log("Promise"); resolve(); }).then(() => { console.log('then'); }); console.log('333');
最新回复(0)