1. 程式人生 > 實用技巧 >《JavaScript高階程式設計》筆記

《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'變為nullJSON.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是取得私有變數值的一種方法,跟閉包沒有嚴格關係。


防抖和節流

視覺化線上demo
滾動欄線上demo

學習連結1
學習連結2

概念

防抖:(停止後才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(),例項化一個新的物件
  • 通過applycall方法呼叫

對應的this的指向:

  • 函式呼叫:windowundefined
  • 方法呼叫:obj物件
  • 建構函式呼叫:例項化的物件
  • apllycall:第一個引數

詳解:

函式呼叫

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原型物件。

  1. Function原型物件也同樣適用此規則:
Function.__proto__ === Function.prototype; // true
Function.prototype.__proto__ == Object.prototype; // true 為了不指向自身的Function.prototype造成迴圈引用
  1. 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相等操作符(==)

參考:
連結1
連結2
連結3

兩組操作符

相等:==(先轉換再比較)
全等:===(僅比較不轉換)

相等(==)規則

Boolean規則:Boolean(val):如果有一個運算元是Boolean值,則在比較前先將其轉換為數值——false0true1
String&Number規則:Number(string):如果一個是String,一個是Number,則先將String轉為Number
Object規則:valueOf(obj):如果有一個是物件,則呼叫valueOf方法(陣列調toString()方法)。

問題探討

[] == []; // false
{} == {}; // false
[] == ![]; // true
{} == !{}; // false

[] == []{} == {}是因為引用的物件指向不同的指標,所以不會相等。

一、[] == ![]

  • 1:邏輯非(!)的優先順序高於相等操作符(==),所以先計算![]booleanfalse,此時比較的是:[] == 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被認為是同一個值,NaNNaN也是相等的。

新增(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]) -
物件或其它 物件或其它 只接受物件 物件或其它 只接受物件
slicesplice delete delete delete delete
清除 arr = [] clear clear clear clear
splice - - - -
includesindexOf gethas gethas 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

第一個丟擲resolvepromise就是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;

exportexport default的區別

  • export需要匯出多個並需要{}export default只需要一個{}匯出全部(沒有額外{});
  • import時,export需要匯入多個,export default是預設的,只需要給一個名字;

require

requireAMD規範;importES6規範。

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]