01. JS加载阻塞 02. 变量提升 03. TDZ(暂存性死区) 04. 块级作用域以及全局污染 05. 严格模式"use strict" 06. 类型检测 07. 隐式转换 08. 正式学习一下Symbol 09. 非常有趣的Set 10. 可以解决不能用对象作键的问题Map 11. 深究函数 12. 任务管理
一般地,一个包含外部样式表和外部脚本问价的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>解析器会在执行代码之前,先解析一遍,在解析的过程中如果有变量的使用在变量的定义之前,那么解析器就会做一步操作,提升变量
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); }) 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的
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); // 500window.screenLeft自动获取窗口距离屏幕左边的距离,由于var的全局污染,导致它是一个定值了,所以我们应该避免使用var
虽然我们了解到了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 word3. 参数不允许使用重复参数名,用就报错
"user strict"; function xxx(name, name) {}; //Duplicate parameter name not allowed in this context当我们给某个函数使用"use strict"时,这个要求会向下传递,也就是说,此函数内部所有内容都使用严格模式,而此函数之外不作要求
类型检测的方法挺多的,我们这之说三种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()都可以精准的返回它的类型
基本上所有类型都可以转为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); }用于储存任何类型的唯一值
只能保存值没有键名严格类型检测(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] } })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' ] }函数就是将复用的代码封装起来
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 function2. 匿名函数
匿名函数就是指函数本身没有名字,只是将这个函数赋值给了一个变量
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 defined4. 函数提升
函数提升 ~= 变量提升(还记得吗??),而且函数提升优先级大于变量提升,但是赋值函数不存在函数提升
// 变量提升 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);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');