《JavaScript高階程式設計》筆記
基本概念
<noscript>
<noscript>
標籤:當頁面不支援script
或禁用了script
時會顯示<noscript>
裡面的內容。
<script>
中的async和defer
1.`<script src="script.js"></script>` 讀到就立即執行。 2.`<script async src="script.js"></script>` 和DOM並行進行(非同步)。 2.`<script defer src="script.js"></script>` 和DOM並行進行(非同步),但在所有`script.js`的執行解析完後,`DOMContentLoaded`事件觸發完成之前。
資料型別
null
是空物件指標,所以typeof null
返回的是object
,
'null'
變為null
:JSON.parse('null')
防篡改物件
preventExtensions
:不能增,能刪改
seal
:不能增刪,能改
freeze
:不能增刪改
物件屬性 | 增 | 刪 | 改 |
---|---|---|---|
preventExtensions | × | ✓ | ✓ |
seal | × | × | ✓ |
freeze | × | × | × |
不可擴充套件物件preventExtensions
Object.preventExtensions不能增,能刪改
var obj = {a:1,b:2}; Object.preventExtensions(obj); obj.c = 3; console.log(obj.c); // undefined delete obj.a; console.log(obj); // {b: 2} 刪除成功 obj.b = 'hello' console.log(obj); // {b: "hello"} 修改成功
檢測是否不可擴充套件Object.isExtensible(obj)
(false是不可擴充套件,true是正常物件)
Object.isExtensible(obj);// false
密封的物件seal
Object.seal不能增刪,能改
var obj = {a:1,b:2}; Object.seal(obj); obj.c = 3; console.log(obj.c); // undefined delete obj.a; console.log(obj); // {a:1,b:2} 刪除失敗 obj.b = 'hello' console.log(obj); // {a:1,b: "hello"} 修改成功
檢測是否密封Object.isSealed(obj)
(false是正常,true是已經密封了)
Object.isSealed(obj);// true
凍結的物件freeze
Object.freeze不能增刪改
var obj = {a:1,b:2};
Object.freeze(obj);
obj.c = 3;
console.log(obj.c); // undefined
delete obj.a;
console.log(obj); // {a:1,b:2} 刪除失敗
obj.b = 'hello'
console.log(obj); // {a:1,b:2} 修改失敗
檢測是否凍結Object.isFrozen(obj)
(false是正常,true是已經凍結了)
Object.isFrozen(obj);// true
setTimeout
// setTimeout的時間假設設定為1000,不是說1秒後立馬會執行,而是儘快執行,把任務新增到了佇列中,如果排到它了,就立馬執行。
遞迴
arguments.callee:函式自身
用arguments.callee
實現遞迴
function test(num) {
console.log(num)
if(num!==0){
--num;
arguments.callee(num)
}
}
test(3)
3
2
1
但是arguments.callee
已經被棄用了,所以可以嘗試其它方法。
命名一個function
function test(num) {
(function fn (){
console.log(num)
if(num !== 0) {
--num;
fn();
}
})();
}
test(3)
return和閉包
直接return
var a = 0;
function fn(){
var a = 12;
return a;
}
console.log(fn()); // 12
console.log(a); // 0
return function
var a = 0;
function fn() {
var a = 12;
return function(){
return a
};
}
console.log(fn()()); // 12
console.log(a); // 0
return 閉包
var a = 0;
function fn() {
var a = 12;
return (function(){
return a
})();
}
console.log(fn()); // 12
console.log(a); // 0
區別
1.直接return返回的是變數,閉包返回的是執行環境(所以在return function部分就要fn()()這樣呼叫2次)。
2.閉包不是為了讓函式外部拿到內部變數,而是為了保護私有變數不被更改。
3.return出來的是一個值(12),不是變數本身(a),此處的return是取得私有變數值的一種方法,跟閉包沒有嚴格關係。
防抖和節流
概念
防抖:(停止後才1次)觸發事件後n秒內只執行1次,如果n秒內又觸發了事件,則會重新計算時間。
節流:(幾秒1次)一定時間內只能執行1次。
應用場景
防抖:
- 搜尋框搜尋輸入,只有使用者停止輸入時,才傳送請求;
- 手機號、郵箱號驗證輸入檢測;
- 視窗resize,只需等視窗調整完成後計算大小,防止重複渲染。
節流:
- 表單驗證時重複點選提交按鈕;
- 滾動載入;
- 瀏覽器搜尋框聯想功能。
實現原理
1、防抖
正常情況下,我希望它多久執行,假設郵箱驗證正常情況是每隔1秒向後臺傳送請求,然後使用者一直不停的在輸入框輸入,此時會不斷的清除Timeout,直到停止呼叫方法1秒後才正常去向後臺傳送請求。
// 防抖【防止多次觸發滾動事件】
var time = '';
handleDebounce () {
console.log('呼叫')
// 清除未執行的程式碼,重置回初始化狀態
if(timer){clearTimeout(timer);}
//開始一個新的任務
timer = setTimeout(()=>{
console.log('函式防抖');
}, 1000);
},
2、節流
假設瀏覽器一直在不停滾動,我不可能等停止了再請求,也不可能一直請求。
var flag = false;
handleThrottle () {
console.log('呼叫')
if(flag){return}
flag = false;
setTimeout(()=>{
console.log('函式節流');
flag = true;
},1000)
}
prototype 和 hasOwnProperty
Array.prototype.arr = function(){console.log('print arr')};
var a = [1,2,3];
a.arr(); // 'print arr'
Array.prototype.hasOwnProperty('arr'); // true
a.hasOwnProperty('arr'); // false
Array.hasOwnProperty('arr'); // false
let和閉包
let劫持作用域
用var時
console.log(str);
var str = 'hello';
打印出undefined
。
相當於
var str ;
console.log(str);
str = 'hello';
用var 的話,變數名會提升,但並不會賦值。
用let時
console.log(str);
let str = 'hello';
報錯VM67161:1 Uncaught ReferenceError: str is not defined
這裡相當於直接console.log('未定義變數名')
,此時的let已經劫持了var的作用域。
用閉包作用域解釋為什麼用let的for迴圈可以劫持資料。
假設我們想每隔1秒分別列印1、2、3、4、5。
for (var i = 1; i < 6; i++) {
console.log(i)
setTimeout(() => {
console.log('print'+i)
}, 1000 * i)
}
會列印1、2、3、4、5,然後每隔1秒列印一次'print6'
.
因為任務流
的關係,console.log(i)會先於setTimeout執行,等for迴圈的6次console執行完之後,佇列裡的setTimeout才會依次執行,而這個時候的i已經是6了。
用let可以劫持i的作用域。
for (var i = 1; i < 6; i++) {
let j = i;
console.log(j)
setTimeout(() => {
console.log('print'+j)
}, 1000 * j)
}
此時就是先列印1、2、3、4、5,然後每隔1秒列印'print1'、'print2'...'print5'。但是,每次都會有新的j替代原來的j,所以可以直接在for迴圈裡面定義let i = 1;
for (var i = 1; i < 6; i++) {
console.log(i)
setTimeout(() => {
console.log('print'+i)
}, 1000 * i)
}
function和object
function
var str = (()=> {
var count = 0;
function sum () { return ++count; };
function reduce () {return --count;};
return {
sum,
reduce
}
})
此時的str是個function
簡化下:
var str = (()=> {
var count = 0;
return {
sum : ()=>{return ++count;},
reduce : ()=>{return --count;}
}
})();
str.sum(); // 1
此時的str是已經立即執行函數了,返回的是Object,是{sum:f,reduce:f},注意,這裡的str是獲取不到count的,只有return的資料能獲取到。
Object
var obj = {
count:0,
sum : ()=>{return ++obj.count;},
reduce : ()=>{return --obj.count;}
}
obj.sum(); // 1
這裡的obj是Object,不同於str的是,它能獲取到count,object裡面的所有資料都能獲取到。
區別
- Function只有return的方法才能獲取到(閉包)
- Function執行後返回的是物件
java物件
// 建立類——“人”
public class People{
int age;
}
// 建立類——“男人”
public class MenPeople extends People {
}
// 建立物件
MenPeople xm = new MenPeople();
xm.age = 15;
js物件
var xm = {age:15}
https://www.jianshu.com/p/edf4d665d0df
https://www.cnblogs.com/yanyunpiaomaio/p/11025444.html
JavaScript函式呼叫及this引數
JS有4種方式呼叫函式
- 作為一個函式(
function
)——fn()
直接被呼叫 - 作為一個方法(
methods
)——obj.fn()
,關聯在物件上呼叫,實現面向物件程式設計 - 作為一個建構函式(
constructor
)——new Fn()
,例項化一個新的物件 - 通過
apply
或call
方法呼叫
對應的this的指向:
- 函式呼叫:
window
或undefined
- 方法呼叫:obj物件
- 建構函式呼叫:例項化的物件
aplly
或call
:第一個引數
詳解:
函式呼叫
function fn(){
console.log(this);
}
fn(); // window
嚴格模式下:
function fn(){
"use strict"
console.log(this);
}
fn(); // undefined
方法呼叫
var obj = {
fn : function(){
console.log(this);
}
};
obj.fn() // 返回obj物件:{fn: ƒ}
建構函式呼叫
function Cat(x,y){
this.x = x;
this.y = y;
console.log(this);
}
var c = new Cat(1,2);
c // Cat{x:1,y:2} 指向c物件
es6寫法
class Point{
constructor(x,y){
this.x = x;
this.y = y;
console.log(this);
}
}
var p = new Point(1,2)
p // Point{x:1,y:2} 指向p物件
aplly或call
var name = '張三';
var age = '24';
var obj = {
name: this.name, // 此處的this指向window
objAge: this.age, // 此處的this指向window
fun: function(fm,t){
console.log(this)
console.log(this.name+'年齡 '+this.age + ' 來自'+fm+' 去往'+t); // 此處的fm和t就是要傳入的引數
}
}
var pd = {
name: '彭丹',
age:18
}
obj.fun.call(pd,'長沙','上海'); // 彭丹 年齡18 來自長沙 去往上海
obj.fun.apply(pd,['長沙','上海']); // 彭丹 年齡18 來自長沙 去往上海
obj.fun.bind(pd,'長沙','上海')(); // 彭丹 年齡18 來自長沙 去往上海
obj.fun.bind(pd,['長沙','上海'])(); // 彭丹 年齡18 來自長沙上海 去往undefined
this
打印出來全都是{name: "彭丹", age: 18}
,就是第一個引數。
函式構造器
與建構函式
名字類似,但無太大關係。
普通生成
var p = new Function('x','y','return x+y');
p(2,3)
動態生成
createFunction(){
let arr = Array.from(arguments);
var params = arr.splice(0,arr.length-1);
var body = arr[0];
return new Function(params,body);
},
test(){
var sum = this.createFunction('x','y','return x + y');
var chen = this.createFunction('x','y','return x * y');
console.log(sum(3,2)) // 5
console.log(chen(3,2)) // 6
},
函式生成器(generator)
function* test(){
console.log(1);
yield;
console.log(2);
}
let item = test();
item.next();
setTimeout(()=>{
item.next();
},3000)
1
隔3秒後
2
JavaScript面向物件
封裝
生成物件
function Cat(name,color){
this.name = name;
this.color = color;
}
var cat1 = new Cat('大貓','黃色');
var cat2 = new Cat('小貓','黑色');
cat1; // Cat {name: "大貓", color: "黃色"} 指向Cat物件
cat2; // Cat {name: "小貓", color: "黑色"} 指向Cat物件
相當於我們平時用的陣列中的:
var arr = new Array(3).fill(2);
var brr = new Array(5).fill(8);
arr; // (3) [2, 2, 2] 指向Array物件
brr; // (5) [8, 8, 8, 8, 8] 指向Array物件
只不過我們平時是直接用var arr = [1,2]
的形式,和new Array
是同一個意思。
物件的建構函式
function Cat(name,color){
this.name = name;
this.color = color;
}
這段程式碼裡面的this.name = name
就是建構函式,可以直接用es6語法糖的形式寫:
es6語法糖class
class Cat{
constructor(x,y){
this.x = x;
this.y = y;
}
}
var cat1 = new Cat('大貓','黃色');
cat1; // Cat {name: "大貓", color: "黃色"} 指向Cat物件
constructor
所以,cat1
例項含有constructor
屬性指向它(Cat)的建構函式
。
cat1.constructor === Cat; // true
相當於我們平時用的陣列中的:
[1,2].constructor === Array; // true
其它
[2].constructor(); // []
[2].constructor() === Array.prototype.constructor();
instanceof
JavaScript
還提供了instanceof
運算子,驗證原型物件(Cat)
與例項物件(cat1)
之間的關係。
cat1 instanceof Cat; // true
相當於我們平時用的陣列中的:
[1,2] instanceof Array; // true
原型物件新增方法
直接新增造成的問題
function Cat(name,color){
this.name = name;
this.color = color;
this.type = '貓科動物';
this.eat = function(){
console.log('吃魚')
}
}
var cat1 = new Cat('大貓','黃色');
var cat2 = new Cat('小貓','黑色');
cat1.eat == cat2.eat; // false
此時eat方法佔用了太多記憶體,並且它們沒有指向同一個引用地址,永遠不會相等。參考陣列的其實是相等的。
[1].push == [2].push; // true
用prototype新增方法
function Cat(name,color){
this.name = name;
this.color = color;
}
Cat.prototype.type = '貓科動物';
Cat.prototype.eat = function(){
console.log('吃魚')
}
var cat1 = new Cat('大貓','黃色');
var cat2 = new Cat('小貓','黑色');
cat1.eat == cat2.eat; // true,它們是指向同一個記憶體地址下的方法
(就算不定義Cat的prototype,Cat也自帶有prototype屬性)
prototype模式的驗證方法
判斷物件和例項的關係isPrototypeOf
Cat.prototype.isPrototypeOf(cat1); // true
相當於我們平時用的陣列中的:
Array.prototype.isPrototypeOf([]); // true
判斷是本地屬性還是prototype屬性
cat1.hasOwnProperty('name'); // true
cat1.hasOwnProperty('type'); // false
in
'name' in cat1; // true
相當於我們平時用的陣列中的:
'push' in []; // true
proto
一般情況下,例項物件的__proto__
指向原型物件的prototype
。
prototype
被例項的__proto__
指向
__proto__
指向建構函式的prototype
__proto__
存在於例項和建構函式的原型物件,而不是例項與建構函式。
如:
cat1.__proto__ === Cat.prototype; // true
相當於我們平時用的陣列中的:
[].__proto__ === Array.prototype; // true
其它情況
function fn(){};
fn.__proto__ === Function.prototype; // true
把函式當作物件時,生成它的函式就是Function
原型物件。
Function
原型物件也同樣適用此規則:
Function.__proto__ === Function.prototype; // true
Function.prototype.__proto__ == Object.prototype; // true 為了不指向自身的Function.prototype造成迴圈引用
Object
函式也是一個Function
函式:
Object.__proto__ === Function.prototype; // true
Object.prototype.__proto__ === null ; // true 為了不指向自身的Object.prototype造成迴圈引用
Object.prototype.__proto__==null
是所有函式的終點
DOM也有原型鏈
<html>
<head>
<title>dom原型測試</title>
</head>
<body>
<div id="test">test dom</div>
<script type="text/javascript">
HTMLElement.prototype.hello = function(){
console.log(this);
}
var div = document.getElementById('test');
div.hello();
</script>
</body>
</html>
Object.create實現類繼承和克隆物件
Object.create實現類繼承
先看不用Object.create來實現繼承
function Pd(){
}
Pd.prototype = Array.prototype;
Pd.prototype.constructor = Pd;
var pdd = new Pd();
pdd.push(3);
console.log(pdd); // Pd [3] __proto__:Array(0)直接就是真正的陣列的__proto__
效果:
此時列印Array.prototype.constructor
會發現變成了undefined
,已經改動了原生的Array
.
用Object.create實現繼承
function Pd(){
}
Pd.prototype = Object.create(Array.prototype);
Pd.prototype.constructor = Pd;
var pdd = new Pd();
pdd.push(3);
console.log(pdd); // Pd [3] __proto__:Array[__proto__:Array(0)]就是__proto__裡面包含真正的陣列的__proto__
效果:
區別
寫法:
Pd.prototype = Array.prototype;
和Pd.prototype = Object.create(Array.prototype);
返回值:
Pd [3] __proto__:Array(0)
直接就是真正的陣列的__proto__
;Pd [3] __proto__:Array[__proto__:Array(0)]
就是__proto__
裡面包含真正的陣列的__proto__
。
用Object.create實現繼承自己的類並帶引數
function Cat(name,color){
this.name = name;
this.color = color;
}
var cat1 = new Cat('大貓','黃色');
function Pd(name,color){
Cat.call(this,name,color);
}
Pd.prototype = Object.create(Cat.prototype);
Pd.prototype.constructor = Pd;
var pdd = new Pd('小貓','白色');
console.log(cat1,pdd); // Cat {name: "大貓", color: "黃色"} Pd {name: "小貓", color: "白色"}
用原生寫法實現繼承自己的類並帶引數
function Cat(name,color){
this.name = name;
this.color = color;
}
var cat1 = new Cat('大貓','黃色');
function Pd(name,color){
Cat.call(this,name,color);
}
Pd.prototype = Cat.prototype;
Pd.prototype.constructor = Pd;
var pdd = new Pd('小貓','白色');
console.log(cat1,pdd); // Cat {name: "大貓", color: "黃色"} Pd {name: "小貓", color: "白色"}
用Object.create克隆物件
var obj1 = {a:2,b:{name:'小明'}};
var obj2 = Object.create(obj1);
console.log(obj2); // {}
obj2.a = 3;
obj2.b.name = '小紅';
console.log(obj1); // {a:2,b:{name:'小紅'}};
結論:obj1物件中的一級物件a:2並沒有受影響,但二級物件b已經受影響。所以Object.create克隆的物件也只能實現一級物件的深拷貝。
obj2的具體值:
extends繼承
class Cat{
constructor(){
console.log('cat');
}
}
class Child extends Cat{
};
var cat = new Cat();
var child = new Child();
繼承所有引數:
class Cat{
constructor(name){
this.name = name;
}
}
class Child extends Cat{
constructor(name){
super(name);
}
};
var cat = new Cat('1');
var child = new Child('2');
console.log(cat,child); // Cat {name: "1"} Child {name: "2"}
new.target
方法判斷是否父類
class Cat{
constructor(){
console.log(new.target);
if (new.target === Cat) {
console.log('父類');
} else {
console.log('子類');
}
}
}
class Child extends Cat{
constructor(){
super();
}
};
var cat = new Cat();
var child = new Child();
new Array()和[]比較
效能
var startTime=new Date().getTime();
var a2 = new Object();
for(var i = 0;i<10000000;i++){
a2[i] = [];
}
var endTime=new Date().getTime();
console.log('[]輸出耗時:',endTime-startTime);
var startTime2=new Date().getTime();
var a = new Object();
for(var i = 0;i<10000000;i++){
a[i] = new Array();
}
var endTime2=new Date().getTime();
console.log('new Array()輸出耗時:',endTime2-startTime2);
結果:
[]輸出耗時: 304
new Array()輸出耗時: 600
每次結果不同,但大約都是new Array()
是[]
的兩倍,時間越大,差距越大。
(最好用時間差相減來計算時間,用console.time
可能會有先後的問題導致不準確。)
寫法
[]
是字面量,JSON格式的語法是引擎直接解釋的;
new Array()
需要呼叫Array
的構造器。
JavaScript相等操作符(==)
兩組操作符
相等:==
(先轉換再比較)
全等:===
(僅比較不轉換)
相等(==
)規則
Boolean規則:Boolean(val):如果有一個運算元是Boolean
值,則在比較前先將其轉換為數值——false
為0
,true
為1
。
String&Number規則:Number(string):如果一個是String
,一個是Number
,則先將String
轉為Number
。
Object規則:valueOf(obj):如果有一個是物件,則呼叫valueOf
方法(陣列調toString()
方法)。
問題探討
[] == []; // false
{} == {}; // false
[] == ![]; // true
{} == !{}; // false
[] == []
和{} == {}
是因為引用的物件指向不同的指標,所以不會相等。
一、[] == ![]
- 1:邏輯非(
!
)的優先順序高於相等操作符(==
),所以先計算![]
的boolean
值false
,此時比較的是:[] == false
; - 2:根據上面提到的boolean規則,則需要把
false
轉成0
,此時比較的是:[] == 0
; - 3:根據上面提到的Object規則,呼叫空陣列的toString方法,即
[].toString()
的值為''
,此時比較的是:'' == 0
; - 4:根據上面提到的String規則,將字串轉為數字,即
Number('')
的值為0
,此時比較的是:0 == 0
。
簡化:
[] == ![]
轉化:[] == false
轉化: [] == 0
轉化'' == 0
轉化: 0 == 0
。
二、{} == !{}
- 1:先計算
!{}
得到false
,此時比較的是:{} == false
; - 2:呼叫Booean規則,計算
Boolean({})
得到true
,此時比較的是true == false
。
簡化:
{} == !{}
轉化:{} == false
轉化:true == false
。
MessageChannel
MessageChannel的基本使用
const {port1, port2} = new MessageChannel();
port1.onmessage = function(d) {
console.log(`port1接收的訊息是:${d.data}`);
}
port2.onmessage = function(d) {
console.log(`port2接收的訊息是:${d.data}`);
}
port1.postMessage('port1傳送的訊息');
port2.postMessage('port2傳送的訊息');
port1傳送的由port2接收,port2傳送的由port1接收。
也就是說,傳過去的物件,接收到的時候已經不是原來的引用和指標了,這個時候再return出來,就是一個新的物件,所以肯定能實現深拷貝。
使用MessageChannel實現深拷貝
var obj = {id:1,name:{a:'xx'}};
function structuralClone(obj) {
return new Promise((resolve) => {
const {port1, port2} = new MessageChannel();
port2.onmessage = ev => resolve(ev.data);
port1.postMessage(obj);
})
}
structuralClone(obj).then(res=>{
console.log(res);
var obj3 = res;
obj3.name.a = 'obj3';
console.log(obj,obj3);
})
<!-- 用promise是為了好傳資料 -->
使用lodash.cloneDeep實現深拷貝
import _ from 'lodash'
var obj = {id:1,name:{a:'xx'},fn:function(){},un:undefined};
var obj2 = _.cloneDeep(obj);
obj2.name.a = 'obj2';
console.log(obj,obj2)
ES6…
擴充套件(spread)/收集(rest)運算子詳解
一、擴充套件運算子
我理解的,用()
包起來就是擴充套件成單個值,用[]
包起來就是擴充套件成陣列。
1.代替apply
var test = function(a,b,c){
console.log(a,b,c);
}
var arr = [1,2,3];
test(...arr); // 1 2 3
用apply的寫法:
test.apply(null,arr);
2.代替concat
var arr1 = [1,2,3,4];
var arr2 = [0,...arr1,5,6];
console.log(arr2); // [0, 1, 2, 3, 4, 5, 6]
用concat的寫法:
[0].concat(arr1,5,6); // [0, 1, 2, 3, 4, 5, 6]
3.代替split
var str = 'hello';
var arr3 = [...str];
console.log(arr3); // ["h", "e", "l", "l", "o"]
用split的寫法:
'hello'.split(''); // ["h", "e", "l", "l", "o"]
二、收集運算子
1.接收不確定個數的形參
此功能和JAVA
一樣,當形參傳入個數不確定時可用在形參上。
var rest2 = function(item, ...arr){
console.log(item,arr);
}
rest2('hello',2,3,3,4); // hello [2, 3, 3, 4]
2.配合解構時使用
var [a,...temp] = [1,2,3,4];
console.log(a,temp); // 1 [2, 3, 4]
arguments
引數的3種轉陣列方法
方法1:Array.prototype.slice.apply
方法2:Array.from
方法3:[...arguments]
var test3 = function(){
console.log(arguments);
var list1 = Array.prototype.slice.apply(arguments);
console.log(list1);
var list2 = Array.from(arguments);
console.log(list2);
var list3 = [...arguments];
console.log(list3);
}
test3(1,2,3,4);
預設引數值
假設想要的效果是這樣的:
var foo = function(x,y){
x = x || 10;
y = y || 20;
console.log(x+y);
}
foo(1,2); // 3
foo(); // 30
但是也有出錯的時候:
foo(0,1); // 11
第一個引數0被解析成了false,而不是數字0進行計算。
用預設引數值
var foo = function(x=10, y=20){
console.log(x+y);
}
foo(0,1); // 1
解構
var obj = {a:1,b:2,c:3},a,b,c,p;
p = {a,b,c} = obj;
console.log(p === obj); // true
Blob實現下載檔案
DOM:
<a id="download" @click="download">下載</a>
JS:
download(){
var blob = new Blob(['hello world']);
var url = window.URL.createObjectURL(blob);
var a = document.getElementById('download');
a.download = 'helloworld.txt';
a.href = url;
},
Map
為什麼要用Map?因為普通資料結構無法以非字串為鍵。
舉例:
var m = {};
var x = {id:1}, y = {id:2};
m[x] = 'foo';
m[y] = 'bar';
console.log(m,m[x],m[y]); // {[object Object]: "bar"} "bar" "bar"
物件m
中只有一個[object Object]
,值都是'bar'
,它無法解析兩個物件為鍵。
使用Map以非字串為鍵
var m = new Map();
var x = {id:1}, y = {id:2};
m.set(x , 'foo');
m.set(y , 'bar');
console.log(m);
console.log(m.get(x));
console.log(m.get(y));
console.log(m.get({id:1}));
結果:
delete刪除
m.delete(y);
clean清除所有
m.clear();
m.size; // 0
size大小
m.size;
new Map
深拷貝
var m2 = m1; // 淺拷貝
var m3 = new Map(m1); // 深拷貝
深拷貝例項:
var mm = new Map();
mm.set('a',{id:1});
var mm2 = new Map(mm);
mm2.set('a', {id:4});
console.log(mm2,mm);
結果:
兩個value值都是物件,互不影響。
Map所有的值
方法1:m.values()
方法2:m.entries()
方法1:m.values()
返回一個迭代器,可以用spread擴充套件運算子(...
)或Array.from()
轉換成陣列。
var m = new Map();
var x = {id:1}, y = {id:2};
m.set(x , 'foo');
m.set(y , 'bar');
console.log(m.values()); // MapIterator {"foo", "bar"}
console.log([...m.values()]); // ["foo", "bar"]
console.log(Array.from(m.values())); // ["foo", "bar"]
方法2:m.entries()
var m = new Map();
var x = {id:1}, y = {id:2};
m.set(x , 'foo');
m.set(y , 'bar');
console.log(m.entries()); // MapIterator {{…} => "foo", {…} => "bar"}
console.log([...m.entries()]); // [[{id: 1},'foo'],[{id: 2},'bar']]
console.log([...m.entries()][0][1]); // "foo"
console.log([...m.entries()][1][1]); // "bar"
Map所有的鍵
keys
var m = new Map();
var x = {id:1}, y = {id:2};
m.set(x , 'foo');
m.set(y , 'bar');
console.log([...m.keys()]); // [{id:1},{id:2}]
has判斷是否有該鍵
var m = new Map();
var x = {id:1}, y = {id:2};
m.set(x , 'foo');
m.set(y , 'bar');
console.log(m.has(y)); // true
WeakMap
區別:
-
內部記憶體(特別是GC)的工作方式;
-
WeakMap只接受物件為鍵;所以物件被回收專案也會移除
var m = new WeakMap();
var x = {id:1}, y = {id:2};
m.set(x ,y);
console.log(m.has(x)); // true
x = null;
console.log(m.has(x)); // false
Set
Set是一個值的集合,其中的值是唯一的。
API:
新建:new Set()
增:add()
刪:delete()
查:has
新建
var s = new Set([0,-0,1,2,NaN,2,3,NaN]);
console.log(s); // Set(5) {0, 1, 2, NaN, 3}
0
和-0
被認為是同一個值,NaN
與NaN
也是相等的。
新增(add)
s.add(7);
console.log(s); // Set(6) {0, 1, 2, NaN, 3, 7}
刪除(delete和clear)
s.delete(2);
console.log(s); // Set(5) {0, 1, NaN, 3, 7}
s.clear();
console.log(s.size); // 0
查詢是否存在(has)
不像Map
裡面的get
能直接取值,這裡是查詢是否存在該值。
s.has(1); // true
迭代
同Map
s.keys(); // SetIterator {0, 1, NaN, 3, 7}
s.values(); // SetIterator {0, 1, NaN, 3, 7}
s.entries(); // SetIterator {0 => 0, 1 => 1, NaN => NaN, 3 => 3, 7 => 7}
雖然keys()
和values()
返回的值一樣,但它們倆並不相等。
s.keys() == s.values(); // false
WeakSet
和Set的區別:
只能存物件
var ws = new WeakSet([1,2,2,3]); // 無效:Uncaught TypeError: Invalid value used in weak set
WeakSet使用:
var obj1 = {id:1};
var obj2 = {id:2};
var ws = new WeakSet();
ws.add(obj1).add(obj2).add(obj1);
console.log(ws); // [{id:1},{id:2}]
添加了obj1
兩次,還是去重了。
GC
obj1 = null;
console.log(ws); // [{id:1},{id:2}]
ws.has(obj1); // false
雖然obj1的值看上去還在,但已經取不到了。
delete刪除
ws.delete(obj2);
console.log(ws); // [{id:1}]
Array、Map、WeakMap、Set、WeakSet的對比
對比表
功能屬性 | Array | Map | WeakMap | Set | WeakSet |
---|---|---|---|---|---|
新建 | [] |
new Map() |
new WeakMap() |
new Set() |
new WeakSet() |
增 | push |
m.set(obj,'value') |
wm.set(obj1,'value') |
s.add(value) |
ws.add(obj) |
新建並增加 | [1,2] |
- | - | new Set([4, 0, 0, 4, 1]) |
- |
鍵 | 物件或其它 | 物件或其它 | 只接受物件 | 物件或其它 | 只接受物件 |
刪 | slice 或splice |
delete |
delete |
delete |
delete |
清除 | arr = [] |
clear |
clear |
clear |
clear |
改 | splice |
- | - | - | - |
查 | includes 、indexOf 等 |
get 或has |
get 或has |
has |
has |
鍵 | m.keys() 下標 |
m.keys() |
- | m.keys() |
- |
值 | m.values() 值 |
m.values() |
- | m.values() |
- |
迭代 | entries |
entries |
- | entries |
- |
長度 | length |
size |
- | size |
- |
Map API:
- size數量
- set()設定
- clear()清除
- delete()刪除
- has()存在
- get()獲取
- keys()鍵
- values()值
- entries()迭代
WeakMap API:
- set()設定
- delete()刪除
- has()存在
- get()獲取
- clear()清除(已棄用,但可通過new WeakMap()空物件來置空)
Set API:
- size數量
- add()新增
- clear()清除
- delete()刪除
- has()存在
- keys()鍵
- values()值
- entries()迭代
WeakSet API:
- add()新增
- delete()刪除
- has()存在
proxy
淺拷貝:什麼也不寫
var obj = {
id:1
};
var p = new Proxy(obj,{});
p.a = 33;
console.log(obj); // {id:1,a:33}
JS執行機制
非同步:現在和將來的時間間隙
並行:能夠同時發生的事情
並行:比如程序與執行緒,獨立執行並且能同時執行。
fun : function(){
func1();
func2();
http1();
http2();
}
多程序
每個tab標籤頁有一個獨立的程序(有的可能會合並)
比如:
- Browser程序:主程序;
- 第三方外掛程序;
- GPU程序;
- 瀏覽器渲染程序。
單執行緒
瀏覽器的渲染程序是多個執行緒
的,是多個,這些執行緒還是一個一個執行完了才執行下一個,所以JS引擎是單執行緒
的。
比如:
- GUI渲染執行緒
- JS引擎執行緒
- 事件觸發執行緒
- 定時觸發器執行緒
瀏覽器渲染流程
沉浸樹(render樹):
- 1.處理HTML標籤構建DOM樹;
- 2.處理CSS標籤構建CSSOM樹;
- 3.DOM和CSSOM樹被組合形成渲染樹(render樹);
- 4.佈局render樹,計算尺寸、位置;
- 5.繪製render樹,繪製頁面畫素資訊;
- 6.發給圖形處理器(GPU),顯示在螢幕上。
CSS是否會阻塞dom樹渲染?
由上面的流程可知,不知阻塞DOM樹,但會阻塞CSSOM樹。
事件迴圈(Event Loop
)、巨集任務(macrotask
)、微任務(microtask
)
事件迴圈(Event Loop
):執行完巨集任務後,將微任務排隊新增任務,執行後再迴圈檢查有沒有巨集任務……所以整個過程稱為事件迴圈。
巨集任務(macrotask
):主程式碼、setTimeout、setInterval
微任務(microtask
):promise、process.nextTick
執行順序:先巨集任務--》執行結束後--》再執行所有微任務--》渲染--》下一個巨集任務
console.log('start');
setTimeout(function() {
console.log('1');
}, 10);
new Promise(resolve => {
console.log('2');
resolve();
setTimeout(() => console.log('3'), 10);
}).then(function() {
console.log('4')
})
console.log('end');
這裡的執行順序就是start-->2-->end-->4-->1-->3
注意:
promise是立即執行的,建立的時候就會執行,不存在將promise推入微任務;
resolve()是表示promise的狀態為fullfilled,相當於只是定義了一個有狀態的promise,並沒有呼叫它;
promise呼叫then的前提是promise的狀態為fullfilled;
只有promise呼叫then的時候,then裡面的函式才會被推入微任務中。
setTimeout相關
setTimeout並不是由JS引擎計數的,因為單執行緒會阻塞,會影響計數的準確,因此通過單獨執行緒來計時並觸發。
setTiemout最小為4,不滿會加成4。
try...catch
無法用於非同步程式碼
同步程式碼
try {
foo();
} catch (error) {
console.log('異常是:'+error)
}
此時會由catch捕捉到異常:
異常是:ReferenceError: foo is not defined
非同步程式碼
function foo(){
setTimeout(()=>{
bar.arr();
},100);
};
try {
foo();
} catch (error) {
console.log(error)
}
此時無法捕捉,而是瀏覽器控制檯報出未捕捉異常。
Uncaught ReferenceError: bar is not defined
對比圖
Promise
promise
代替callback
回撥。
promise.all
只能同時呼叫不受關聯的prmise,如果promise2的值受promise1影響,不能用promise.all,可以用async/await
首先假設要依次呼叫3個promise
的程式碼:
var pro1 = new Promise((resolve,reject) => {
console.log(1);
resolve('hello')
})
var pro2 = new Promise((resolve,reject) => {
console.log(2);
setTimeout(()=>{
resolve('world')
},1000);
})
var pro3 = new Promise((resolve,reject) => {
console.log(3);
setTimeout(()=>{
resolve('pdd')
},2000);
})
如果不用promise.all
來呼叫的話:
pro1.then((res1)=>{
});
pro2.then((res2)=>{
})
pro3.then((res3)=>{
})
只有不停的用.then
才能保證每一步都正確,此時使用promise.all
:
Promise.all([pro1,pro2,pro3]).then(val=>{
console.log(val);
})
promise.race
第一個丟擲resolve
的promise
就是Promise.race
獲取的值。
這種模式稱為門閂模式、promise中稱中競態。
var pro2 = new Promise((resolve,reject) => {
console.log(2);
setTimeout(()=>{
resolve('world')
},1000);
})
var pro3 = new Promise((resolve,reject) => {
console.log(3);
setTimeout(()=>{
resolve('pdd')
},2000);
})
Promise.race([pro2,pro3]).then(val=>{
console.log(val);
})
此時,pro2要花費1秒,pro3要花費2秒,誰先resolve
,.then
獲取的val
就是誰的。
async/await
普通函式和async的區別
普通函式:
function testAsync(){
return 'hello world'
}
testAsync(); // 'hello world'
async
函式:
async function testAsync(){
return 'hello world'
}
testAsync(); // Promise {<fulfilled>: "hello world"}
async
返回的是一個promise
物件
await
如果不用async/await:
async function testAsync(){
return new Promise(resolve => {
setTimeout(()=>resolve('long_time_value'), 1000);
})
}
testAsync().then(v=>{
console.log('get',v);
})
1秒後:get long_time_value
如果用的話:
function testAsync(){
return new Promise(resolve => {
setTimeout(()=>resolve('long_time_value'), 1000);
})
}
async function test(){
const v = await testAsync();
console.log(v);
}
test();
1秒後:get long_time_value
優勢:處理then鏈
function takeLongTime(n){
return new Promise(resolve => {
setTimeout(()=> resolve(n+200), n);
})
}
function step1(n) {
console.log(`step1 with ${n}`);
return takeLongTime(n);
}
function step2(n) {
console.log(`step2 with ${n}`);
return takeLongTime(n);
}
function step3(n) {
console.log(`step3 with ${n}`);
return takeLongTime(n);
}
function doIt(){
console.time("doIt");
const time1 = 3000;
step1(time1)
.then(time2 => step2(time2))
.then(time3 => step3(time3))
.then(result => {
console.log(`result is ${result}`);
console.timeEnd("doIt");
});
}
doIt();
step1 with 3000
VM5329:13 step2 with 3200
VM5329:18 step3 with 3400
VM5329:29 result is 3600
VM5329:30 doIt: 9606.429931640625ms
每一個promise都受上一個promise影響,所以必須一個調完之後再調另外一個。
再看看用async/await更改doIt方法:
async function doIt(){
console.time("doIt");
const time1 = 3000;
const time2 = await step1(time1);
const time3 = await step2(time2);
const result = await step3(time3);
console.log(`result is ${result}`);
console.timeEnd("doIt");
}
doIt();
結果和上一個不停用then
鏈的一樣,但是程式碼要清晰得多,而且沒有回撥地獄。
export 和 import 和 require
普通使用
constant.js
var constant = {
edit:"編輯",
test:'2'
}
var b = {};
export {
constant,
b
};
test.vue
import {constant,b} from '@/utils/test';
console.log(constant,b)
全域性使用
constant.js
export default {
list1:[],
list2:[],
b:function(){}
}
main.js
improt constant from './utils/test';
Vue.prototype.$constant = constant;
test.vue
this.list = this.$constant.list1;
export
和export default
的區別
export
需要匯出多個並需要{}
,export default
只需要一個{}
匯出全部(沒有額外{}
);import
時,export
需要匯入多個,export default
是預設的,只需要給一個名字;
require
require
是AMD
規範;import
是ES6
規範。
require
是賦值,import
是解構。
defaultImg2: require("../../../assets/img/default.png"),
flat()
和flatMap()
flat
拉平陣列,預設一層,填幾就拉平幾層巢狀,如果想拉平所有的,用Infinity
[1, 2, [3, [4, 5]]].flat()
// [1, 2, 3, [4, 5]]
[1, 2, [3, [4, 5]]].flat(2)
// [1, 2, 3, 4, 5]
[1, [2, [3]]].flat(Infinity)
// [1, 2, 3]
flatMap
與map類似,不同的是可以拉平陣列,但只能拉平一層,不能多層。
[1,[2,[3],4,5],6,[7],8].flatMap(v => v*2)
(5) [2, NaN, 12, 14, 16]