上篇
html
首先 typeof 可以正確的判斷基本數據類型,可是除了 null, typeof null輸出的是對象。前端
可是對象來講,typeof 不能正確的判斷其類型, typeof 一個函數能夠輸出 'function',而除此以外,輸出的全是 object,這種狀況下,咱們沒法準確的知道對象的類型。node
instanceof能夠準確的判斷複雜數據類型,可是不能正確判斷基本數據類型。nginx
instanceof 是經過原型鏈判斷的,A instanceof B, 在A的原型鏈中層層查找,是否有原型等於B.prototype,若是一直找到A的原型鏈的頂端(null;即Object.prototype.__proto__
),仍然不等於B.prototype,那麼返回false,不然返回true.git
instanceof的實現代碼:github
// L instanceof R function instance_of(L, R) {//L 表示左表達式,R 表示右表達式 var O = R.prototype;// 取 R 的顯式原型 L = L.__proto__; // 取 L 的隱式原型 while (true) { if (L === null) //已經找到頂層 return false; if (O === L) //當 O 嚴格等於 L 時,返回 true return true; L = L.__proto__; //繼續向上一層原型鏈查找 } }複製代碼
PS: Object.keys():返回給定對象全部可枚舉web
關於forEach是否會改變原數組的問題,有些小夥伴提出了異議,爲此我寫了代碼測試了下(注意數組項是複雜數據類型的狀況)。 除了forEach以外,map等API,也有一樣的問題。面試
let arry = [1, 2, 3, 4]; arry.forEach((item) => { item *= 10; }); console.log(arry); //[1, 2, 3, 4] arry.forEach((item) => { arry[1] = 10; //直接操做數組 }); console.log(arry); //[ 1, 10, 3, 4 ] let arry2 = [ { name: "Yve" }, { age: 20 } ]; arry2.forEach((item) => { item.name = 10; }); console.log(arry2);//[ { name: 10 }, { age: 20, name: 10 } ]複製代碼
arr.constructor === Array
. (不許確,由於咱們能夠指定 obj.constructor = Array
)function fn() { console.log(Array.isArray(arguments)); //false; 由於arguments是類數組,但不是數組 console.log(Array.isArray([1,2,3,4])); //true console.log(arguments instanceof Array); //fasle console.log([1,2,3,4] instanceof Array); //true console.log(Object.prototype.toString.call(arguments)); //[object Arguments] console.log(Object.prototype.toString.call([1,2,3,4])); //[object Array] console.log(arguments.constructor === Array); //false arguments.constructor = Array; console.log(arguments.constructor === Array); //true console.log(Array.isArray(arguments)); //false } fn(1,2,3,4);複製代碼
類數組:express
1)擁有length屬性,其它屬性(索引)爲非負整數(對象中的索引會被當作字符串來處理);編程
2)不具備數組所具備的方法;
類數組是一個普通對象,而真實的數組是Array類型。
常見的類數組有: 函數的參數 arugments, DOM 對象列表(好比經過 document.querySelectorAll 獲得的列表), jQuery 對象 (好比 $("div")).
類數組能夠轉換爲數組:
//第一種方法
Array.prototype.slice.call(arrayLike, start);
//第二種方法
[...arrayLike];
//第三種方法:
Array.from(arrayLike);複製代碼
PS: 任何定義了遍歷器(Iterator)接口的對象,均可以用擴展運算符轉爲真正的數組。
Array.from方法用於將兩類對象轉爲真正的數組:相似數組的對象(array-like object)和可遍歷(iterable)的對象。
=== 不須要進行類型轉換,只有類型相同而且值相等時,才返回 true.
== 若是二者類型不一樣,首先須要進行類型轉換。具體流程以下:
let person1 = { age: 25 } let person2 = person1; person2.gae = 20; console.log(person1 === person2); //true,注意複雜數據類型,比較的是引用地址複製代碼
咱們來分析一下: [] == ![]
是true仍是false?
![]
引用類型轉換成布爾值都是true,所以![]
的是false修改原數組的API有:
splice/reverse/fill/copyWithin/sort/push/pop/unshift/shift
不修改原數組的API有:
slice/map/forEach/every/filter/reduce/entries/find
注: 數組的每一項是簡單數據類型,且未直接操做數組的狀況下(稍後會對此題從新做答)。
變量提高就是變量在聲明以前就可使用,值爲undefined。
在代碼塊內,使用 let/const 命令聲明變量以前,該變量都是不可用的(會拋出錯誤)。這在語法上,稱爲「暫時性死區」。暫時性死區也意味着 typeof 再也不是一個百分百安全的操做。
typeof x; // ReferenceError(暫時性死區,拋錯) let x;複製代碼
typeof y; // 值是undefined,不會報錯複製代碼
暫時性死區的本質就是,只要一進入當前做用域,所要使用的變量就已經存在了,可是不可獲取,只有等到聲明變量的那一行代碼出現,才能夠獲取和使用該變量。
this的綁定規則有四種:默認綁定,隱式綁定,顯式綁定,new綁定.
測試下是否已經成功Get了此知識點(瀏覽器執行環境):
var number = 5; var obj = { number: 3, fn1: (function () { var number; this.number *= 2; number = number * 2; number = 3; return function () { var num = this.number; this.number *= 2; console.log(num); number *= 3; console.log(number); } })() } var fn1 = obj.fn1; fn1.call(null); obj.fn1(); console.log(window.number);複製代碼
執行上下文就是當前 JavaScript 代碼被解析和執行時所在環境, JS執行上下文棧能夠認爲是一個存儲函數調用的棧結構,遵循先進後出的原則。
做用域鏈: 不管是 LHS 仍是 RHS 查詢,都會在當前的做用域開始查找,若是沒有找到,就會向上級做用域繼續查找目標標識符,每次上升一個做用域,一直到全局做用域爲止。
題難不難?
閉包是指有權訪問另外一個函數做用域中的變量的函數,建立閉包最經常使用的方式就是在一個函數內部建立另外一個函數。
閉包的做用有:
call 和 apply 的功能相同,區別在於傳參的方式不同:
fn.call(obj, arg1, arg2, ...),調用一個函數, 具備一個指定的this值和分別地提供的參數(參數的列表)。
fn.apply(obj, [argsArray]),調用一個函數,具備一個指定的this值,以及做爲一個數組(或類數組對象)提供的參數。
call核心:
Function.prototype.call = function (context) { /** 若是第一個參數傳入的是 null 或者是 undefined, 那麼指向this指向 window/global */ /** 若是第一個參數傳入的不是null或者是undefined, 那麼必須是一個對象 */ if (!context) { //context爲null或者是undefined context = typeof window === 'undefined' ? global : window; } context.fn = this; //this指向的是當前的函數(Function的實例) let rest = [...arguments].slice(1);//獲取除了this指向對象之外的參數, 空數組slice後返回的仍然是空數組 let result = context.fn(...rest); //隱式綁定,當前函數的this指向了context. delete context.fn; return result; } //測試代碼 var foo = { name: 'Selina' } var name = 'Chirs'; function bar(job, age) { console.log(this.name); console.log(job, age); } bar.call(foo, 'programmer', 20); // Selina programmer 20 bar.call(null, 'teacher', 25); // 瀏覽器環境: Chirs teacher 25; node 環境: undefined teacher 25 複製代碼
apply:
apply的實現和call很相似,可是須要注意他們的參數是不同的,apply的第二個參數是數組或類數組.
Function.prototype.apply = function (context, rest) { if (!context) { //context爲null或者是undefined時,設置默認值 context = typeof window === 'undefined' ? global : window; } context.fn = this; let result; if(rest === undefined || rest === null) { //undefined 或者 是 null 不是 Iterator 對象,不能被 ... result = context.fn(rest); }else if(typeof rest === 'object') { result = context.fn(...rest); } delete context.fn; return result; } var foo = { name: 'Selina' } var name = 'Chirs'; function bar(job, age) { console.log(this.name); console.log(job, age); } bar.apply(foo, ['programmer', 20]); // Selina programmer 20 bar.apply(null, ['teacher', 25]); // 瀏覽器環境: Chirs programmer 20; node 環境: undefined teacher 25複製代碼
bind
bind 和 call/apply 有一個很重要的區別,一個函數被 call/apply 的時候,會直接調用,可是 bind 會建立一個新函數。當這個新函數被調用時,bind() 的第一個參數將做爲它運行時的 this,以後的一序列參數將會在傳遞的實參前傳入做爲它的參數。
Function.prototype.bind = function(context) { if(typeof this !== "function"){ throw new TypeError("not a function"); } let self = this; let args = [...arguments].slice(1); function Fn() {}; Fn.prototype = this.prototype; let bound = function() { let res = [...args, ...arguments]; //bind傳遞的參數和函數調用時傳遞的參數拼接 context = this instanceof Fn ? this : context || this; return self.apply(context, res); } //原型鏈 bound.prototype = new Fn(); return bound; } var name = 'Jack'; function person(age, job, gender){ console.log(this.name , age, job, gender); } var Yve = {name : 'Yvette'}; let result = person.bind(Yve, 22, 'enginner')('female'); 複製代碼
new:
function new(func) { let target = {}; target.__proto__ = func.prototype; let res = func.call(target); if (typeof(res) == "object" || typeof(res) == "function") { return res; } return target; }複製代碼
字面量建立對象,不會調用 Object構造函數, 簡潔且性能更好;
new Object() 方式建立對象本質上是方法調用,涉及到在proto鏈中遍歷該方法,當找到該方法後,又會生產方法調用必須的 堆棧信息,方法調用結束後,還要釋放該堆棧,性能不如字面量的方式。
經過對象字面量定義對象時,不會調用Object構造函數。
在 JavaScript 中,每當定義一個對象(函數也是對象)時候,對象中都會包含一些預約義的屬性。其中每一個函數對象都有一個prototype 屬性,這個屬性指向函數的原型對象。使用原型對象的好處是全部對象實例共享它所包含的屬性和方法。
原型鏈解決的主要是繼承問題。
每一個對象擁有一個原型對象,經過 proto (讀音: dunder proto) 指針指向其原型對象,並從中繼承方法和屬性,同時原型對象也可能擁有原型,這樣一層一層,最終指向 null(Object.proptotype.__proto__
指向的是null)。這種關係被稱爲原型鏈 (prototype chain),經過原型鏈一個對象能夠擁有定義在其餘對象中的屬性和方法。
構造函數 Parent、Parent.prototype 和 實例 p 的關係以下:(p.__proto__ === Parent.prototype)
prototype是構造函數的屬性。
__proto__
是每一個實例都有的屬性,能夠訪問 [[prototype]] 屬性。
實例的__proto__
與其構造函數的prototype指向的是同一個對象。
function Student(name) { this.name = name; } Student.prototype.setAge = function(){ this.age=20; } let Jack = new Student('jack'); console.log(Jack.__proto__); //console.log(Object.getPrototypeOf(Jack));; console.log(Student.prototype); console.log(Jack.__proto__ === Student.prototype);//true複製代碼
組合繼承(最經常使用的繼承方式)
function SuperType() { this.name = name; this.colors = ['red', 'blue', 'green']; } SuperType.prototype.sayName = function() { console.log(this.name); } function SubType(name, age) { SuperType.call(this, name); this.age = age; } SubType.prototype = new SuperType(); SubType.prototype.constructor = SubType; SubType.prototype.sayAge = function() { console.log(this.age); }複製代碼
其它繼承方式實現,能夠參考《JavaScript高級程序設計》
淺拷貝是指只複製第一層對象,可是當對象的屬性是引用類型時,實質複製的是其引用,當引用指向的值改變時也會跟着變化。
深拷貝複製變量值,對於非基本類型的變量,則遞歸至基本類型變量後,再複製。深拷貝後的對象與原來的對象是徹底隔離的,互不影響,對一個對象的修改並不會影響另外一個對象。
實現一個深拷貝:
function deepClone(obj) { //遞歸拷貝 if(obj === null) return null; //null 的狀況 if(obj instanceof RegExp) return new RegExp(obj); if(obj instanceof Date) return new Date(obj); if(typeof obj !== 'object') { //若是不是複雜數據類型,直接返回 return obj; } /** * 若是obj是數組,那麼 obj.constructor 是 [Function: Array] * 若是obj是對象,那麼 obj.constructor 是 [Function: Object] */ let t = new obj.constructor(); for(let key in obj) { //若是 obj[key] 是複雜數據類型,遞歸 t[key] = deepClone(obj[key]); } return t; }複製代碼
看不下去了?別人的
防抖和節流的做用都是防止函數屢次調用。區別在於,假設一個用戶一直觸發這個函數,且每次觸發函數的間隔小於設置的時間,防抖的狀況下只會調用一次,而節流的狀況會每隔必定時間調用一次函數。
防抖(debounce): n秒內函數只會執行一次,若是n秒內高頻事件再次被觸發,則從新計算時間
function debounce(func, wait, immediate=true) { let timeout, context, args; // 延遲執行函數 const later = () => setTimeout(() => { // 延遲函數執行完畢,清空定時器 timeout = null // 延遲執行的狀況下,函數會在延遲函數中執行 // 使用到以前緩存的參數和上下文 if (!immediate) { func.apply(context, args); context = args = null; } }, wait); let debounced = function (...params) { if (!timeout) { timeout = later(); if (immediate) { //當即執行 func.apply(this, params); } else { //閉包 context = this; args = params; } } else { clearTimeout(timeout); timeout = later(); } } debounced.cancel = function () { clearTimeout(timeout); timeout = null; }; return debounced; };複製代碼
防抖的應用場景:
節流(throttle): 高頻事件在規定時間內只會執行一次,執行一次後,只有大於設定的執行週期後纔會執行第二次。
//underscore.js function throttle(func, wait, options) { var timeout, context, args, result; var previous = 0; if (!options) options = {}; var later = function () { previous = options.leading === false ? 0 : Date.now() || new Date().getTime(); timeout = null; result = func.apply(context, args); if (!timeout) context = args = null; }; var throttled = function () { var now = Date.now() || new Date().getTime(); if (!previous && options.leading === false) previous = now; var remaining = wait - (now - previous); context = this; args = arguments; if (remaining <= 0 || remaining > wait) { if (timeout) { clearTimeout(timeout); timeout = null; } previous = now; result = func.apply(context, args); if (!timeout) context = args = null; } else if (!timeout && options.trailing !== false) { // 判斷是否設置了定時器和 trailing timeout = setTimeout(later, remaining); } return result; }; throttled.cancel = function () { clearTimeout(timeout); previous = 0; timeout = context = args = null; }; return throttled; }; 複製代碼
函數節流的應用場景有:
// ES5 的寫法 Math.max.apply(null, [14, 3, 77, 30]); // ES6 的寫法 Math.max(...[14, 3, 77, 30]); // reduce [14,3,77,30].reduce((accumulator, currentValue)=>{ return accumulator = accumulator > currentValue ? accumulator : currentValue });複製代碼
setTimeout() 只是將事件插入了「任務隊列」,必須等當前代碼(執行棧)執行完,主線程纔會去執行它指定的回調函數。要是當前代碼消耗時間很長,也有可能要等好久,因此並沒辦法保證回調函數必定會在 setTimeout() 指定的時間執行。因此, setTimeout() 的第二個參數表示的是最少時間,並不是是確切時間。
HTML5標準規定了 setTimeout() 的第二個參數的最小值不得小於4毫秒,若是低於這個值,則默認是4毫秒。在此以前。老版本的瀏覽器都將最短期設爲10毫秒。另外,對於那些DOM的變更(尤爲是涉及頁面從新渲染的部分),一般是間隔16毫秒執行。這時使用 requestAnimationFrame() 的效果要好於 setTimeout();
0.1 + 0.2 != 0.3 是由於在進制轉換和進階運算的過程當中出現精度損失。
下面是詳細解釋:
JavaScript使用 Number 類型表示數字(整數和浮點數),使用64位表示一個數字。
圖片說明:
計算機沒法直接對十進制的數字進行運算, 須要先對照 IEEE 754 規範轉換成二進制,而後對階運算。
1.進制轉換
0.1和0.2轉換成二進制後會無限循環
0.1 -> 0.0001100110011001...(無限循環)
0.2 -> 0.0011001100110011...(無限循環)
複製代碼複製代碼
可是因爲IEEE 754尾數位數限制,須要將後面多餘的位截掉,這樣在進制之間的轉換中精度已經損失。
2.對階運算
因爲指數位數不相同,運算時須要對階運算 這部分也可能產生精度損失。
按照上面兩步運算(包括兩步的精度損失),最後的結果是
0.0100110011001100110011001100110011001100110011001100
結果轉換成十進制以後就是 0.30000000000000004。
promise有三種狀態: fulfilled, rejected, pending.
Promise 的優勢:
Promise 的缺點:
Promise的構造函數是同步執行的。then 中的方法是異步執行的。
promise的then實現,
Promise 是微任務,setTimeout 是宏任務,同一個事件循環中,promise.then老是先於 setTimeout 執行。
要實現 Promise.all,首先咱們須要知道 Promise.all 的功能:
Promise.all = function (promises) { return new Promise((resolve, reject) => { let index = 0; let result = []; if (promises.length === 0) { resolve(result); } else { function processValue(i, data) { result[i] = data; if (++index === promises.length) { resolve(result); } } for (let i = 0; i < promises.length; i++) { //promises[i] 多是普通值 Promise.resolve(promises[i]).then((data) => { processValue(i, data); }, (err) => { reject(err); return; }); } } }); } 複製代碼複製代碼
無論成功仍是失敗,都會走到finally中,而且finally以後,還能夠繼續then。而且會將值原封不動的傳遞給後面的then.
Promise.prototype.finally = function (callback) { return this.then((value) => { return Promise.resolve(callback()).then(() => { return value; }); }, (err) => { return Promise.resolve(callback()).then(() => { throw err; }); }); } 複製代碼複製代碼
函數柯里化是把接受多個參數的函數變換成接受一個單一參數(最初函數的第一個參數)的函數,而且返回接受餘下的參數並且返回結果的新函數的技術。
function sum(a) { return function(b) { return function(c) { return a+b+c; } } } console.log(sum(1)(2)(3)); // 6 複製代碼複製代碼
引伸:實現一個curry函數,將普通函數進行柯里化:
function curry(fn, args = []) { return function(){ let rest = [...args, ...arguments]; if (rest.length < fn.length) { return curry.call(this,fn,rest); }else{ return fn.apply(this,rest); } } } //test function sum(a,b,c) { return a+b+c; } let sumFn = curry(sum); console.log(sumFn(1)(2)(3)); //6 console.log(sumFn(1)(2, 3)); //6複製代碼
本文轉載自 前端小姐姐
異步最先的解決方案是回調函數,如事件的回調,setInterval/setTimeout中的回調。可是回調函數有一個很常見的問題,就是回調地獄的問題(稍後會舉例說明);
爲了解決回調地獄的問題,社區提出了Promise解決方案,ES6將其寫進了語言標準。Promise解決了回調地獄的問題,可是Promise也存在一些問題,如錯誤不能被try catch,並且使用Promise的鏈式調用,其實並無從根本上解決回調地獄的問題,只是換了一種寫法。
ES6中引入 Generator 函數,Generator是一種異步編程解決方案,Generator 函數是協程在 ES6 的實現,最大特色就是能夠交出函數的執行權,Generator 函數能夠看出是異步任務的容器,須要暫停的地方,都用yield語句註明。可是 Generator 使用起來較爲複雜。
ES7又提出了新的異步解決方案:async/await,async是 Generator 函數的語法糖,async/await 使得異步代碼看起來像同步代碼,異步編程發展的目標就是讓異步邏輯的代碼看起來像同步同樣。
1.回調函數: callback
//node讀取文件 fs.readFile(xxx, 'utf-8', function(err, data) { //code });複製代碼
回調函數的使用場景(包括但不限於):
異步回調嵌套會致使代碼難以維護,而且不方便統一處理錯誤,不能try catch 和 回調地獄(如先讀取A文本內容,再根據A文本內容讀取B再根據B的內容讀取C...)。
fs.readFile(A, 'utf-8', function(err, data) { fs.readFile(B, 'utf-8', function(err, data) { fs.readFile(C, 'utf-8', function(err, data) { fs.readFile(D, 'utf-8', function(err, data) { //.... }); }); }); });複製代碼
2.Promise
Promise 主要解決了回調地獄的問題,Promise 最先由社區提出和實現,ES6 將其寫進了語言標準,統一了用法,原生提供了Promise對象。
那麼咱們看看Promise是如何解決回調地獄問題的,仍然以上文的readFile爲例。
function read(url) { return new Promise((resolve, reject) => { fs.readFile(url, 'utf8', (err, data) => { if(err) reject(err); resolve(data); }); }); } read(A).then(data => { return read(B); }).then(data => { return read(C); }).then(data => { return read(D); }).catch(reason => { console.log(reason); });複製代碼
想要運行代碼看效果,請戳(VS的 Code Runner 執行代碼): github.com/YvetteLau/B…
思考一下在Promise以前,你是如何處理異步併發問題的,假設有這樣一個需求:讀取三個文件內容,都讀取成功後,輸出最終的結果。有了Promise以後,又如何處理呢?代碼可戳: github.com/YvetteLau/B…
注: 可使用 bluebird 將接口 promise化;
引伸: Promise有哪些優勢和問題呢?
3.Generator
Generator 函數是 ES6 提供的一種異步編程解決方案,整個 Generator 函數就是一個封裝的異步任務,或者說是異步任務的容器。異步操做須要暫停的地方,都用 yield 語句註明。
Generator 函數通常配合 yield 或 Promise 使用。Generator函數返回的是迭代器。對生成器和迭代器不瞭解的同窗,請自行補習下基礎。下面咱們看一下 Generator 的簡單使用:
function* gen() { let a = yield 111; console.log(a); let b = yield 222; console.log(b); let c = yield 333; console.log(c); let d = yield 444; console.log(d); } let t = gen(); //next方法能夠帶一個參數,該參數就會被看成上一個yield表達式的返回值 t.next(1); //第一次調用next函數時,傳遞的參數無效 t.next(2); //a輸出2; t.next(3); //b輸出2; t.next(4); //c輸出3; t.next(5); //d輸出3;複製代碼
爲了讓你們更好的理解上面代碼是如何執行的,我畫了一張圖,分別對應每一次的next方法調用:
仍然以上文的readFile爲例,使用 Generator + co庫來實現:
const fs = require('fs'); const co = require('co'); const bluebird = require('bluebird'); const readFile = bluebird.promisify(fs.readFile); function* read() { yield readFile(A, 'utf-8'); yield readFile(B, 'utf-8'); yield readFile(C, 'utf-8'); //.... } co(read()).then(data => { //code }).catch(err => { //code }); 複製代碼
不使用co庫,如何實現?可否本身寫一個最簡的my_co?請戳: github.com/YvetteLau/B…
PS: 若是你還不太瞭解 Generator/yield,建議閱讀ES6相關文檔。
4.async/await
ES7中引入了 async/await 概念。async實際上是一個語法糖,它的實現就是將Generator函數和自動執行器(co),包裝在一個函數中。
async/await 的優勢是代碼清晰,不用像 Promise 寫不少 then 鏈,就能夠處理回調地獄的問題。錯誤能夠被try catch。
仍然以上文的readFile爲例,使用 Generator + co庫來實現:
const fs = require('fs'); const bluebird = require('bluebird'); const readFile = bluebird.promisify(fs.readFile); async function read() { await readFile(A, 'utf-8'); await readFile(B, 'utf-8'); await readFile(C, 'utf-8'); //code } read().then((data) => { //code }).catch(err => { //code });複製代碼
可執行代碼,請戳:github.com/YvetteLau/B…
思考一下 async/await 如何處理異步併發問題的? github.com/YvetteLau/B…
若是你有更好的答案或想法,歡迎在這題目對應的github下留言:說一說JS異步發展史
async/await 就是 Generator 的語法糖,使得異步操做變得更加方便。來張圖對比一下:
async 函數就是將 Generator 函數的星號(*)替換成 async,將 yield 替換成await。
咱們說 async 是 Generator 的語法糖,那麼這個糖究竟甜在哪呢?
1)async函數內置執行器,函數調用以後,會自動執行,輸出最後結果。而Generator須要調用next或者配合co模塊使用。
2)更好的語義,async和await,比起星號和yield,語義更清楚了。async表示函數裏有異步操做,await表示緊跟在後面的表達式須要等待結果。
3)更廣的適用性。co模塊約定,yield命令後面只能是 Thunk 函數或 Promise 對象,而async 函數的 await 命令後面,能夠是 Promise 對象和原始類型的值。
4)返回值是Promise,async函數的返回值是 Promise 對象,Generator的返回值是 Iterator,Promise 對象使用起來更加方便。
async 函數的實現原理,就是將 Generator 函數和自動執行器,包裝在一個函數裏。
具體代碼試下以下(和spawn的實現略有差別,我的以爲這樣寫更容易理解),若是你想知道如何一步步寫出 my_co ,可戳: github.com/YvetteLau/B…
function my_co(it) { return new Promise((resolve, reject) => { function next(data) { try { var { value, done } = it.next(data); }catch(e){ return reject(e); } if (!done) { //done爲true,表示迭代完成 //value 不必定是 Promise,多是一個普通值。使用 Promise.resolve 進行包裝。 Promise.resolve(value).then(val => { next(val); }, reject); } else { resolve(value); } } next(); //執行一次next }); } function* test() { yield new Promise((resolve, reject) => { setTimeout(resolve, 100); }); yield new Promise((resolve, reject) => { // throw Error(1); resolve(10) }); yield 10; return 1000; } my_co(test()).then(data => { console.log(data); //輸出1000 }).catch((err) => { console.log('err: ', err); });複製代碼
若是你有更好的答案或想法,歡迎在這題目對應的github下留言:談談對 async/await 的理解,async/await 的實現原理是什麼?
try...catch
中。//下面兩種寫法均可以同時觸發 //法一 async function f1() { await Promise.all([ new Promise((resolve) => { setTimeout(resolve, 600); }), new Promise((resolve) => { setTimeout(resolve, 600); }) ]) } //法二 async function f2() { let fn1 = new Promise((resolve) => { setTimeout(resolve, 800); }); let fn2 = new Promise((resolve) => { setTimeout(resolve, 800); }) await fn1; await fn2; }複製代碼
/** * 函數a內部運行了一個異步任務b()。當b()運行的時候,函數a()不會中斷,而是繼續執行。 * 等到b()運行結束,可能a()早就* 運行結束了,b()所在的上下文環境已經消失了。 * 若是b()或c()報錯,錯誤堆棧將不包括a()。 */ function b() { return new Promise((resolve, reject) => { setTimeout(resolve, 200) }); } function c() { throw Error(10); } const a = () => { b().then(() => c()); }; a(); /** * 改爲async函數 */ const m = async () => { await b(); c(); }; m(); 複製代碼
報錯信息以下,能夠看出 async 函數能夠保留運行堆棧。
若是你有更好的答案或想法,歡迎在這題目對應的github下留言:使用 async/await 須要注意什麼?
在代碼實現前,咱們須要先了解 Promise.race 的特色:
Promise.race返回的仍然是一個Promise. 它的狀態與第一個完成的Promise的狀態相同。它能夠是完成( resolves),也能夠是失敗(rejects),這要取決於第一個Promise是哪種狀態。
若是傳入的參數是不可迭代的,那麼將會拋出錯誤。
若是傳的參數數組是空,那麼返回的 promise 將永遠等待。
若是迭代包含一個或多個非承諾值和/或已解決/拒絕的承諾,則 Promise.race 將解析爲迭代中找到的第一個值。
Promise.race = function (promises) { //promises 必須是一個可遍歷的數據結構,不然拋錯 return new Promise((resolve, reject) => { if (typeof promises[Symbol.iterator] !== 'function') { //真實不是這個錯誤 Promise.reject('args is not iteratable!'); } if (promises.length === 0) { return; } else { for (let i = 0; i < promises.length; i++) { Promise.resolve(promises[i]).then((data) => { resolve(data); return; }, (err) => { reject(err); return; }); } } }); }複製代碼
測試代碼:
//一直在等待態 Promise.race([]).then((data) => { console.log('success ', data); }, (err) => { console.log('err ', err); }); //拋錯 Promise.race().then((data) => { console.log('success ', data); }, (err) => { console.log('err ', err); }); Promise.race([ new Promise((resolve, reject) => { setTimeout(() => { resolve(100) }, 1000) }), new Promise((resolve, reject) => { setTimeout(() => { resolve(200) }, 200) }), new Promise((resolve, reject) => { setTimeout(() => { reject(100) }, 100) }) ]).then((data) => { console.log(data); }, (err) => { console.log(err); });複製代碼
引伸: Promise.all/Promise.reject/Promise.resolve/Promise.prototype.finally/Promise.prototype.catch 的實現原理,若是還不太會,戳:Promise源碼實現
若是你有更好的答案或想法,歡迎在這題目對應的github下留言:如何實現 Promise.race?
一個對象若是要具有可被 for...of 循環調用的 Iterator 接口,就必須在其 Symbol.iterator 的屬性上部署遍歷器生成方法(或者原型鏈上的對象具備該方法)
PS: 遍歷器對象根本特徵就是具備next方法。每次調用next方法,都會返回一個表明當前成員的信息對象,具備value和done兩個屬性。
//如爲對象添加Iterator 接口; let obj = { name: "Yvette", age: 18, job: 'engineer', [Symbol.iterator]() { const self = this; const keys = Object.keys(self); let index = 0; return { next() { if (index < keys.length) { return { value: self[keys[index++]], done: false }; } else { return { value: undefined, done: true }; } } }; } }; for(let item of obj) { console.log(item); //Yvette 18 engineer }複製代碼
使用 Generator 函數(遍歷器對象生成函數)簡寫 Symbol.iterator 方法,能夠簡寫以下:
let obj = { name: "Yvette", age: 18, job: 'engineer', * [Symbol.iterator] () { const self = this; const keys = Object.keys(self); for (let index = 0;index < keys.length; index++) { yield self[keys[index]];//yield表達式僅能使用在 Generator 函數中 } } };複製代碼
原生具有 Iterator 接口的數據結構以下。
若是你有更好的答案或想法,歡迎在這題目對應的github下留言:可遍歷數據結構的有什麼特色?
在 requestAnimationFrame 以前,咱們主要使用 setTimeout/setInterval 來編寫JS動畫。
編寫動畫的關鍵是循環間隔的設置,一方面,循環間隔足夠短,動畫效果才能顯得平滑流暢;另外一方面,循環間隔還要足夠長,才能確保瀏覽器有能力渲染產生的變化。
大部分的電腦顯示器的刷新頻率是60HZ,也就是每秒鐘重繪60次。大多數瀏覽器都會對重繪操做加以限制,不超過顯示器的重繪頻率,由於即便超過那個頻率用戶體驗也不會提高。所以,最平滑動畫的最佳循環間隔是 1000ms / 60 ,約爲16.7ms。
setTimeout/setInterval 有一個顯著的缺陷在於時間是不精確的,setTimeout/setInterval 只能保證延時或間隔不小於設定的時間。由於它們實際上只是把任務添加到了任務隊列中,可是若是前面的任務尚未執行完成,它們必需要等待。
requestAnimationFrame 纔有的是系統時間間隔,保持最佳繪製效率,不會由於間隔時間太短,形成過分繪製,增長開銷;也不會由於間隔時間太長,使用動畫卡頓不流暢,讓各類網頁動畫效果可以有一個統一的刷新機制,從而節省系統資源,提升系統性能,改善視覺效果。
綜上所述,requestAnimationFrame 和 setTimeout/setInterval 在編寫動畫時相比,優勢以下:
1.requestAnimationFrame 不須要設置時間,採用系統時間間隔,能達到最佳的動畫效果。
2.requestAnimationFrame 會把每一幀中的全部DOM操做集中起來,在一次重繪或迴流中就完成。
3.當 requestAnimationFrame() 運行在後臺標籤頁或者隱藏的 <iframe>
裏時,requestAnimationFrame() 會被暫停調用以提高性能和電池壽命(大多數瀏覽器中)。
requestAnimationFrame 使用(試試使用requestAnimationFrame寫一個移動的小球,從A移動到B初):
function step(timestamp) { //code... window.requestAnimationFrame(step); } window.requestAnimationFrame(step); 複製代碼
若是你有更好的答案或想法,歡迎在這題目對應的github下留言:requestAnimationFrame 和 setTimeout/setInterval 有什麼區別?使用 requestAnimationFrame 有哪些好處?
類型轉換的規則三言兩語說不清,真想哇得一聲哭出來~
JS中類型轉換分爲 強制類型轉換 和 隱式類型轉換 。
經過 Number()、parseInt()、parseFloat()、toString()、String()、Boolean(),進行強制類型轉換。
邏輯運算符(&&、 ||、 !)、運算符(+、-、*、/)、關係操做符(>、 <、 <= 、>=)、相等運算符(==)或者 if/while 的條件,可能會進行隱式類型轉換。
強制類型轉換
1.Number() 將任意類型的參數轉換爲數值類型
規則以下:
NAN
0X
/ 0x
開頭的十六進制數字字符串,容許包含正負號),則將其轉換爲十進制NaN
valueOf()
方法,而後依據前面的規則轉換返回的值。若是轉換的結果是 NaN
,則調用對象的 toString()
方法,再次依照前面的規則轉換返回的字符串值。部份內置對象調用默認的 valueOf
的行爲:
對象 | 返回值 |
---|---|
Array | 數組自己(對象類型) |
Boolean | 布爾值(原始類型) |
Date | 從 UTC 1970 年 1 月 1 日午夜開始計算,到所封裝的日期所通過的毫秒數 |
Function | 函數自己(對象類型) |
Number | 數字值(原始類型) |
Object | 對象自己(對象類型) |
String | 字符串值(原始類型) |
Number('0111'); //111 Number('0X11') //17 Number(null); //0 Number(''); //0 Number('1a'); //NaN Number(-0X11);//-17複製代碼
2.parseInt(param, radix)
若是第一個參數傳入的是字符串類型:
若是第一個參數傳入的Number類型:
若是第一個參數是 null 或者是 undefined,或者是一個對象類型:
若是第一個參數是數組: 1. 去數組的第一個元素,按照上面的規則進行解析
若是第一個參數是Symbol類型: 1. 拋出錯誤
若是指定radix參數,以radix爲基數進行解析
parseInt('0111'); //111 parseInt(0111); //八進制數 73 parseInt('');//NaN parseInt('0X11'); //17 parseInt('1a') //1 parseInt('a1'); //NaN parseInt(['10aa','aaa']);//10 parseInt([]);//NaN; parseInt(undefined);複製代碼
parseFloat
規則和parseInt
基本相同,接受一個Number類型或字符串,若是是字符串中,那麼只有第一個小數點是有效的。
toString()
規則以下:
''
[object Object]
let arry = []; let obj = {a:1}; let sym = Symbol(100); let date = new Date(); let fn = function() {console.log('穩住,咱們能贏!')} let str = 'hello world'; console.log([].toString()); // '' console.log([1, 2, 3, undefined, 5, 6].toString());//1,2,3,,5,6 console.log(arry.toString()); // 1,2,3 console.log(obj.toString()); // [object Object] console.log(date.toString()); // Sun Apr 21 2019 16:11:39 GMT+0800 (CST) console.log(fn.toString());// function () {console.log('穩住,咱們能贏!')} console.log(str.toString());// 'hello world' console.log(sym.toString());// Symbol(100) console.log(undefined.toString());// 拋錯 console.log(null.toString());// 拋錯複製代碼
String()
String()
的轉換規則與 toString()
基本一致,最大的一點不一樣在於 null
和 undefined
,使用 String 進行轉換,null 和 undefined對應的是字符串 'null'
和 'undefined'
Boolean
除了 undefined、 null、 false、 ''、 0(包括 +0,-0)、 NaN 轉換出來是false,其它都是true.
隱式類型轉換
&& 、|| 、 ! 、 if/while 的條件判斷
須要將數據轉換成 Boolean 類型,轉換規則同 Boolean 強制類型轉換
運算符: + - * /
+
號操做符,不只能夠用做數字相加,還能夠用做字符串拼接。
僅當 +
號兩邊都是數字時,進行的是加法運算。若是兩邊都是字符串,直接拼接,無需進行隱式類型轉換。
除了上面的狀況外,若是操做數是對象、數值或者布爾值,則調用toString()方法取得字符串值(toString轉換規則)。對於 undefined 和 null,分別調用String()顯式轉換爲字符串,而後再進行拼接。
console.log({}+10); //[object Object]10
console.log([1, 2, 3, undefined, 5, 6] + 10);//1,2,3,,5,610複製代碼
-
、*
、/
操做符針對的是運算,若是操做值之一不是數值,則被隱式調用Number()函數進行轉換。若是其中有一個轉換除了爲NaN,結果爲NaN.
關係操做符: ==、>、< 、<=、>=
>
, <
,<=
,>=
注: NaN是很是特殊的值,它不和任何類型的值相等,包括它本身,同時它與任何類型的值比較大小時都返回false。
console.log(10 > {});//返回false. /** *{}.valueOf ---> {} *{}.toString() ---> '[object Object]' ---> NaN *NaN 和 任何類型比大小,都返回 false */複製代碼
相等操做符:
==
對象如何轉換成原始數據類型
若是部署了 [Symbol.toPrimitive] 接口,那麼調用此接口,若返回的不是基礎數據類型,跑出錯誤。
若是沒有部署 [Symbol.toPrimitive] 接口,那麼先返回 valueOf() 的值,若返回的不是基礎類型的值,再返回 toString() 的值,若返回的不是基礎類型的值, 則拋出異常。
//先調用 valueOf, 後調用 toString let obj = { [Symbol.toPrimitive]() { return 200; }, valueOf() { return 300; }, toString() { return 'Hello'; } } //若是 valueOf 返回的不是基本數據類型,則會調用 toString, //若是 toString 返回的也不是基本數據類型,會拋出錯誤 console.log(obj + 200); //400複製代碼
若是你有更好的答案或想法,歡迎在這題目對應的github下留言:JS 類型轉換的規則是什麼?
HTML5則提出了 Web Worker 標準,表示js容許多線程,可是子線程徹底受主線程控制而且不能操做dom,只有主線程能夠操做dom,因此js本質上依然是單線程語言。
web worker就是在js單線程執行的基礎上開啓一個子線程,進行程序處理,而不影響主線程的執行,當子線程執行完以後再回到主線程上,在這個過程當中不影響主線程的執行。子線程與主線程之間提供了數據交互的接口postMessage和onmessage,來進行數據發送和接收。
var worker = new Worker('./worker.js'); //建立一個子線程 worker.postMessage('Hello'); worker.onmessage = function (e) { console.log(e.data); //Hi worker.terminate(); //結束線程 };複製代碼
//worker.js onmessage = function (e) { console.log(e.data); //Hello postMessage("Hi"); //向主進程發送消息 };複製代碼
僅是最簡示例代碼,項目中一般是將一些耗時較長的代碼,放在子線程中運行。
若是你有更好的答案或想法,歡迎在這題目對應的github下留言:簡述下對 webWorker 的理解
ES6模塊在編譯時,就能肯定模塊的依賴關係,以及輸入和輸出的變量。
CommonJS 模塊,運行時加載。
ES6 模塊自動採用嚴格模式,不管模塊頭部是否寫了 "use strict";
(嚴格模式有哪些限制?[//連接])
require 能夠作動態加載,import 語句作不到,import 語句必須位於頂層做用域中。
ES6 模塊中頂層的 this 指向 undefined,ommonJS 模塊的頂層 this 指向當前模塊。
CommonJS 模塊輸出的是一個值的拷貝,ES6 模塊輸出的是值的引用。
CommonJS 模塊輸出的是值的拷貝,也就是說,一旦輸出一個值,模塊內部的變化就影響不到這個值。如:
//name.js var name = 'William'; setTimeout(() => name = 'Yvette', 200); module.exports = { name }; //index.js const name = require('./name'); console.log(name); //William setTimeout(() => console.log(name), 300); //William複製代碼
對比 ES6 模塊看一下:
ES6 模塊的運行機制與 CommonJS 不同。JS 引擎對腳本靜態分析的時候,遇到模塊加載命令 import ,就會生成一個只讀引用。等到腳本真正執行時,再根據這個只讀引用,到被加載的那個模塊裏面去取值。
//name.js var name = 'William'; setTimeout(() => name = 'Yvette', 200); export { name }; //index.js import { name } from './name'; console.log(name); //William setTimeout(() => console.log(name), 300); //Yvette複製代碼
若是你有更好的答案或想法,歡迎在這題目對應的github下留言:ES6模塊和CommonJS模塊的差別?
在說瀏覽器事件代理機制原理以前,咱們首先了解一下事件流的概念,早期瀏覽器,IE採用的是事件捕獲事件流,而Netscape採用的則是事件捕獲。"DOM2級事件"把事件流分爲三個階段,捕獲階段、目標階段、冒泡階段。現代瀏覽器也都遵循此規範。
那麼事件代理是什麼呢?
事件代理又稱爲事件委託,在祖先級DOM元素綁定一個事件,當觸發子孫級DOM元素的事件時,利用事件冒泡的原理來觸發綁定在祖先級DOM的事件。由於事件會從目標元素一層層冒泡至document對象。
爲何要事件代理?
添加到頁面上的事件數量會影響頁面的運行性能,若是添加的事件過多,會致使網頁的性能降低。採用事件代理的方式,能夠大大減小注冊事件的個數。
事件代理的當時,某個子孫元素是動態增長的,不須要再次對其進行事件綁定。
不用擔憂某個註冊了事件的DOM元素被移除後,可能沒法回收其事件處理程序,咱們只要把事件處理程序委託給更高層級的元素,就能夠避免此問題。
如將頁面中的全部click事件都代理到document上:
addEventListener 接受3個參數,分別是要處理的事件名、處理事件程序的函數和一個布爾值。布爾值默認爲false。表示冒泡階段調用事件處理程序,若設置爲true,表示在捕獲階段調用事件處理程序。
document.addEventListener('click', function (e) { console.log(e.target); /** * 捕獲階段調用調用事件處理程序,eventPhase是 1; * 處於目標,eventPhase是2 * 冒泡階段調用事件處理程序,eventPhase是 1; */ console.log(e.eventPhase); });複製代碼
若是你有更好的答案或想法,歡迎在這題目對應的github下留言:瀏覽器事件代理機制的原理是什麼?
自定義 DOM 事件(不考慮IE9以前版本)
自定義事件有三種方法,一種是使用 new Event()
, 另外一種是 createEvent('CustomEvent')
, 另外一種是 new customEvent()
new Event()
獲取不到 event.detail
let btn = document.querySelector('#btn'); let ev = new Event('alert', { bubbles: true, //事件是否冒泡;默認值false cancelable: true, //事件可否被取消;默認值false composed: false }); btn.addEventListener('alert', function (event) { console.log(event.bubbles); //true console.log(event.cancelable); //true console.log(event.detail); //undefined }, false); btn.dispatchEvent(ev);複製代碼
createEvent('CustomEvent')
(DOM3)要建立自定義事件,能夠調用 createEvent('CustomEvent')
,返回的對象有 initCustomEvent 方法,接受如下四個參數:
let btn = document.querySelector('#btn'); let ev = btn.createEvent('CustomEvent'); ev.initCustomEvent('alert', true, true, 'button'); btn.addEventListener('alert', function (event) { console.log(event.bubbles); //true console.log(event.cancelable);//true console.log(event.detail); //button }, false); btn.dispatchEvent(ev); 複製代碼複製代碼
new customEvent()
(DOM4)使用起來比 createEvent('CustomEvent')
更加方便
var btn = document.querySelector('#btn'); /* * 第一個參數是事件類型 * 第二個參數是一個對象 */ var ev = new CustomEvent('alert', { bubbles: 'true', cancelable: 'true', detail: 'button' }); btn.addEventListener('alert', function (event) { console.log(event.bubbles); //true console.log(event.cancelable);//true console.log(event.detail); //button }, false); btn.dispatchEvent(ev);複製代碼
自定義非 DOM 事件(觀察者模式)
EventTarget類型有一個單獨的屬性handlers,用於存儲事件處理程序(觀察者)。
addHandler() 用於註冊給定類型事件的事件處理程序;
fire() 用於觸發一個事件;
removeHandler() 用於註銷某個事件類型的事件處理程序。
function EventTarget(){ this.handlers = {}; } EventTarget.prototype = { constructor:EventTarget, addHandler:function(type,handler){ if(typeof this.handlers[type] === "undefined"){ this.handlers[type] = []; } this.handlers[type].push(handler); }, fire:function(event){ if(!event.target){ event.target = this; } if(this.handlers[event.type] instanceof Array){ const handlers = this.handlers[event.type]; handlers.forEach((handler)=>{ handler(event); }); } }, removeHandler:function(type,handler){ if(this.handlers[type] instanceof Array){ const handlers = this.handlers[type]; for(var i = 0,len = handlers.length; i < len; i++){ if(handlers[i] === handler){ break; } } handlers.splice(i,1); } } } //使用 function handleMessage(event){ console.log(event.message); } //建立一個新對象 var target = new EventTarget(); //添加一個事件處理程序 target.addHandler("message", handleMessage); //觸發事件 target.fire({type:"message", message:"Hi"}); //Hi //刪除事件處理程序 target.removeHandler("message",handleMessage); //再次觸發事件,沒有事件處理程序 target.fire({type:"message",message: "Hi"});複製代碼
若是你有更好的答案或想法,歡迎在這題目對應的github下留言:js如何自定義事件?
知其然知其因此然,在說跨域方法以前,咱們先了解下什麼叫跨域,瀏覽器有同源策略,只有當「協議」、「域名」、「端口號」都相同時,才能稱之爲是同源,其中有一個不一樣,便是跨域。
那麼同源策略的做用是什麼呢?同源策略限制了從同一個源加載的文檔或腳本如何與來自另外一個源的資源進行交互。這是一個用於隔離潛在惡意文件的重要安全機制。
那麼咱們又爲何須要跨域呢?一是前端和服務器分開部署,接口請求須要跨域,二是咱們可能會加載其它網站的頁面做爲iframe內嵌。
跨域的方法有哪些?
經常使用的跨域方法
儘管瀏覽器有同源策略,可是 <script>
標籤的 src 屬性不會被同源策略所約束,能夠獲取任意服務器上的腳本並執行。jsonp 經過插入script標籤的方式來實現跨域,參數只能經過url傳入,僅能支持get請求。
實現原理:
Step1: 建立 callback 方法
Step2: 插入 script 標籤
Step3: 後臺接受到請求,解析前端傳過去的 callback 方法,返回該方法的調用,而且數據做爲參數傳入該方法
Step4: 前端執行服務端返回的方法調用
下面代碼僅爲說明 jsonp 原理,項目中請使用成熟的庫。分別看一下前端和服務端的簡單實現:
//前端代碼 function jsonp({url, params, cb}) { return new Promise((resolve, reject) => { //建立script標籤 let script = document.createElement('script'); //將回調函數掛在 window 上 window[cb] = function(data) { resolve(data); //代碼執行後,刪除插入的script標籤 document.body.removeChild(script); } //回調函數加在請求地址上 params = {...params, cb} //wb=b&cb=show let arrs = []; for(let key in params) { arrs.push(`${key}=${params[key]}`); } script.src = `${url}?${arrs.join('&')}`; document.body.appendChild(script); }); } //使用 function sayHi(data) { console.log(data); } jsonp({ url: 'http://localhost:3000/say', params: { //code }, cb: 'sayHi' }).then(data => { console.log(data); });複製代碼
//express啓動一個後臺服務 let express = require('express'); let app = express(); app.get('/say', (req, res) => { let {cb} = req.query; //獲取傳來的callback函數名,cb是key res.send(`${cb}('Hello!')`); }); app.listen(3000);複製代碼
從今天起,jsonp的原理就要了然於心啦~
jsonp 只能支持 get 請求,cors 能夠支持多種請求。cors 並不須要前端作什麼工做。
簡單跨域請求:
只要服務器設置的Access-Control-Allow-Origin Header和請求來源匹配,瀏覽器就容許跨域
//簡單跨域請求 app.use((req, res, next) => { res.setHeader('Access-Control-Allow-Origin', 'XXXX'); });複製代碼
帶預檢(Preflighted)的跨域請求
不滿於簡單跨域請求的,便是帶預檢的跨域請求。服務端須要設置 Access-Control-Allow-Origin (容許跨域資源請求的域) 、 Access-Control-Allow-Methods (容許的請求方法) 和 Access-Control-Allow-Headers (容許的請求頭)
app.use((req, res, next) => { res.setHeader('Access-Control-Allow-Origin', 'XXX'); res.setHeader('Access-Control-Allow-Headers', 'XXX'); //容許返回的頭 res.setHeader('Access-Control-Allow-Methods', 'XXX');//容許使用put方法請求接口 res.setHeader('Access-Control-Max-Age', 6); //預檢的存活時間 if(req.method === "OPTIONS") { res.end(); //若是method是OPTIONS,不作處理 } });複製代碼
更多CORS的知識能夠訪問: HTTP訪問控制(CORS)
使用nginx反向代理實現跨域,只須要修改nginx的配置便可解決跨域問題。
A網站向B網站請求某個接口時,向B網站發送一個請求,nginx根據配置文件接收這個請求,代替A網站向B網站來請求。 nginx拿到這個資源後再返回給A網站,以此來解決了跨域問題。
例如nginx的端口號爲 8090,須要請求的服務器端口號爲 3000。(localhost:8090 請求 localhost:3000/say)
nginx配置以下:
server { listen 8090; server_name localhost; location / { root /Users/liuyan35/Test/Study/CORS/1-jsonp; index index.html index.htm; } location /say { rewrite ^/say/(.*)$ /$1 break; proxy_pass http://localhost:3000; add_header 'Access-Control-Allow-Origin' '*'; add_header 'Access-Control-Allow-Credentials' 'true'; add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS'; } # others }複製代碼
Websocket 是 HTML5 的一個持久化的協議,它實現了瀏覽器與服務器的全雙工通訊,同時也是跨域的一種解決方案。
Websocket 不受同源策略影響,只要服務器端支持,無需任何配置就支持跨域。
前端頁面在 8080 的端口。
let socket = new WebSocket('ws://localhost:3000'); //協議是ws socket.onopen = function() { socket.send('Hi,你好'); } socket.onmessage = function(e) { console.log(e.data) }複製代碼
服務端 3000端口。能夠看出websocket無需作跨域配置。
let WebSocket = require('ws'); let wss = new WebSocket.Server({port: 3000}); wss.on('connection', function(ws) { ws.on('message', function(data) { console.log(data); //接受到頁面發來的消息'Hi,你好' ws.send('Hi'); //向頁面發送消息 }); });複製代碼
postMessage 經過用做前端頁面以前的跨域,如父頁面與iframe頁面的跨域。window.postMessage方法,容許跨窗口通訊,不論這兩個窗口是否同源。
話說工做中兩個頁面以前須要通訊的狀況並很少,我本人工做中,僅使用過兩次,一次是H5頁面中發送postMessage信息,ReactNative的webview中接收此此消息,並做出相應處理。另外一次是可輪播的頁面,某個輪播頁使用的是iframe頁面,爲了解決滑動的事件衝突,iframe頁面中去監聽手勢,發送消息告訴父頁面是否左滑和右滑。
子頁面向父頁面發消息
父頁面
window.addEventListener('message', (e) => { this.props.movePage(e.data); }, false);複製代碼
子頁面(iframe):
if(/*左滑*/) { window.parent && window.parent.postMessage(-1, '*') }else if(/*右滑*/){ window.parent && window.parent.postMessage(1, '*') }複製代碼
父頁面向子頁面發消息
父頁面:
let iframe = document.querySelector('#iframe'); iframe.onload = function() { iframe.contentWindow.postMessage('hello', 'http://localhost:3002'); }複製代碼
子頁面:
window.addEventListener('message', function(e) { console.log(e.data); e.source.postMessage('Hi', e.origin); //回消息 });複製代碼
node 中間件的跨域原理和nginx代理跨域,同源策略是瀏覽器的限制,服務端沒有同源策略。
node中間件實現跨域的原理以下:
1.接受客戶端請求
2.將請求 轉發給服務器。
3.拿到服務器 響應 數據。
4.將 響應 轉發給客戶端。
不經常使用跨域方法
如下三種跨域方式不多用,若有興趣,可自行查閱相關資料。
window.name + iframe
location.hash + iframe
document.domain (主域需相同)
若是你有更好的答案或想法,歡迎在這題目對應的github下留言:跨域的方法有哪些?原理是什麼?
<script>
的 defer 屬性,HTML4 中新增
<script>
的 async 屬性,HTML5 中新增
<script>
標籤打開defer屬性,腳本就會異步加載。渲染引擎遇到這一行命令,就會開始下載外部腳本,但不會等它下載和執行,而是直接執行後面的命令。
defer 和 async 的區別在於: defer要等到整個頁面在內存中正常渲染結束,纔會執行;
async一旦下載完,渲染引擎就會中斷渲染,執行這個腳本之後,再繼續渲染。defer是「渲染完再執行」,async是「下載完就執行」。
若是有多個 defer 腳本,會按照它們在頁面出現的順序加載。
多個async腳本是不能保證加載順序的。
function downloadJS() { varelement = document.createElement("script"); element.src = "XXX.js"; document.body.appendChild(element); } //什麼時候的時候,調用上述方法 複製代碼
如頁面 onload 以後,
若是你有更好的答案或想法,歡迎在這題目對應的github下留言:js異步加載的方式有哪些?
//? if(a == 1 && a == 2 && a == 3) { console.log(1); } 複製代碼複製代碼
1.在類型轉換的時候,咱們知道了對象如何轉換成原始數據類型。若是部署了 [Symbol.toPrimitive],那麼返回的就是Symbol.toPrimitive的返回值。固然,咱們也能夠把此函數部署在valueOf或者是toString接口上,效果相同。
//利用閉包延長做用域的特性 let a = { [Symbol.toPrimitive]: (function() { let i = 1; return function() { return i++; } })() }複製代碼
(1). 比較 a == 1 時,會調用 [Symbol.toPrimitive],此時 i 是 1,相等。 (2). 繼續比較 a == 2,調用 [Symbol.toPrimitive],此時 i 是 2,相等。 (3). 繼續比較 a == 3,調用 [Symbol.toPrimitive],此時 i 是 3,相等。
2.利用Object.definePropert在window/global上定義a屬性,獲取a屬性時,會調用get.
let val = 1; Object.defineProperty(window, 'a', { get: function() { return val++; } }); 複製代碼複製代碼
3.利用數組的特性。
var a = [1,2,3];
a.join = a.shift;複製代碼
數組的 toString
方法返回一個字符串,該字符串由數組中的每一個元素的 toString() 返回值經調用 join() 方法鏈接(由逗號隔開)組成。
所以,咱們能夠從新 join 方法。返回第一個元素,並將其刪除。
若是你有更好的答案或想法,歡迎在這題目對應的github下留言:下面代碼a在什麼狀況中打印出1?
function Foo() { getName = function() {console.log(1)}; return this; } Foo.getName = function() {console.log(2)}; Foo.prototype.getName = function() {console.log(3)}; var getName = function() {console.log(4)}; function getName() {console.log(5)}; Foo.getName(); getName(); Foo().getName(); getName(); new Foo.getName(); new Foo().getName(); new new Foo().getName();複製代碼
**說明:**一道經典的面試題,僅是爲了幫助你們回顧一下知識點,加深理解,真實工做中,是不可能這樣寫代碼的,不然,確定會被打死的。
1.首先預編譯階段,變量聲明與函數聲明提高至其對應做用域的最頂端。
所以上面的代碼編譯後以下(函數聲明的優先級先於變量聲明):
function Foo() { getName = function() {console.log(1)}; return this; } var getName; function getName() {console.log(5)}; Foo.getName = function() {console.log(2)}; Foo.prototype.getName = function() {console.log(3)}; getName = function() {console.log(4)};複製代碼
2.Foo.getName()
;直接調用Foo上getName方法,輸出2
3.getName()
;輸出4,getName被從新賦值了
4.Foo().getName()
;執行Foo(),window的getName被從新賦值,返回this;瀏覽器環境中,非嚴格模式,this 指向 window,this.getName();輸出爲1.
若是是嚴格模式,this 指向 undefined,此處會拋出錯誤。
若是是node環境中,this 指向 global,node的全局變量並不掛在global上,由於global.getName對應的是undefined,不是一個function,會拋出錯誤。
5.getName()
;已經拋錯的天然走不動這一步了;繼續瀏覽器非嚴格模式;window.getName被從新賦過值,此時再調用,輸出的是1
6.new Foo.getName()
;考察運算符優先級的知識,new 無參數列表,對應的優先級是18;成員訪問操做符 .
, 對應的優先級是 19。所以至關因而 new (Foo.getName)()
;new操做符會執行構造函數中的方法,所以此處輸出爲 2.
7.new Foo().getName()
;new 帶參數列表,對應的優先級是19,和成員訪問操做符.
優先級相同。同級運算符,按照從左到右的順序依次計算。new Foo()
先初始化 Foo 的實例化對象,實例上沒有getName方法,所以須要原型上去找,即找到了 Foo.prototype.getName
,輸出3
8.new new Foo().getName()
; new 帶參數列表,優先級19,所以至關因而 new (new Foo()).getName()
;先初始化 Foo 的實例化對象,而後將其原型上的 getName 函數做爲構造函數再次 new ,輸出3
所以最終結果以下:
Foo.getName(); //2
getName();//4
Foo().getName();//1
getName();//1
new Foo.getName();//2
new Foo().getName();//3
new new Foo().getName();//3複製代碼
若是你有更好的答案或想法,歡迎在這題目對應的github下留言:下面這段代碼的輸出是什麼?
Object.definedProperty 的做用是劫持一個對象的屬性,劫持屬性的getter和setter方法,在對象的屬性發生變化時進行特定的操做。而 Proxy 劫持的是整個對象。
Proxy 會返回一個代理對象,咱們只須要操做新對象便可,而 Object.defineProperty
只能遍歷對象屬性直接修改。
Object.definedProperty 不支持數組,更準確的說是不支持數組的各類API,由於若是僅僅考慮arry[i] = value 這種狀況,是能夠劫持的,可是這種劫持意義不大。而 Proxy 能夠支持數組的各類API。
儘管 Object.defineProperty 有諸多缺陷,可是其兼容性要好於 Proxy.
PS: Vue2.x 使用 Object.defineProperty 實現數據雙向綁定,V3.0 則使用了 Proxy.
//攔截器 let obj = {}; let temp = 'Yvette'; Object.defineProperty(obj, 'name', { get() { console.log("讀取成功"); return temp }, set(value) { console.log("設置成功"); temp = value; } }); obj.name = 'Chris'; console.log(obj.name);複製代碼
PS: Object.defineProperty 定義出來的屬性,默認是不可枚舉,不可更改,不可配置【沒法delete】
咱們能夠看到 Proxy 會劫持整個對象,讀取對象中的屬性或者是修改屬性值,那麼就會被劫持。可是有點須要注意,複雜數據類型,監控的是引用地址,而不是值,若是引用地址沒有改變,那麼不會觸發set。
let obj = {name: 'Yvette', hobbits: ['travel', 'reading'], info: { age: 20, job: 'engineer' }}; let p = new Proxy(obj, { get(target, key) { //第三個參數是 proxy, 通常不使用 console.log('讀取成功'); return Reflect.get(target, key); }, set(target, key, value) { if(key === 'length') return true; //若是是數組長度的變化,返回。 console.log('設置成功'); return Reflect.set([target, key, value]); } }); p.name = 20; //設置成功 p.age = 20; //設置成功; 不須要事先定義此屬性 p.hobbits.push('photography'); //讀取成功;注意不會觸發設置成功 p.info.age = 18; //讀取成功;不會觸發設置成功複製代碼
最後,咱們再看下對於數組的劫持,Object.definedProperty 和 Proxy 的差異
Object.definedProperty 能夠將數組的索引做爲屬性進行劫持,可是僅支持直接對 arry[i] 進行操做,不支持數組的API,很是雞肋。
let arry = [] Object.defineProperty(arry, '0', { get() { console.log("讀取成功"); return temp }, set(value) { console.log("設置成功"); temp = value; } }); arry[0] = 10; //觸發設置成功 arry.push(10); //不能被劫持複製代碼
Proxy 能夠監聽到數組的變化,支持各類API。注意數組的變化觸發get和set可能不止一次,若有須要,自行根據key值決定是否要進行處理。
let hobbits = ['travel', 'reading']; let p = new Proxy(hobbits, { get(target, key) { // if(key === 'length') return true; //若是是數組長度的變化,返回。 console.log('讀取成功'); return Reflect.get(target, key); }, set(target, key, value) { // if(key === 'length') return true; //若是是數組長度的變化,返回。 console.log('設置成功'); return Reflect.set([target, key, value]); } }); p.splice(0,1) //觸發get和set,能夠被劫持 p.push('photography');//觸發get和set p.slice(1); //觸發get;由於 slice 是不會修改原數組的複製代碼
若是你有更好的答案或想法,歡迎在這題目對應的github下留言:實現雙向綁定 Proxy 與 Object.defineProperty 相比優劣如何?
如下狀況,Object.is認爲是相等
兩個值都是 undefined 兩個值都是 null 兩個值都是 true 或者都是 false 兩個值是由相同個數的字符按照相同的順序組成的字符串 兩個值指向同一個對象 兩個值都是數字而且 都是正零 +0 都是負零 -0 都是 NaN 都是除零和 NaN 外的其它同一個數字複製代碼
Object.is() 相似於 ===,可是有一些細微差異,以下:
console.log(Object.is(NaN, NaN));//true console.log(NaN === NaN);//false console.log(Object.is(-0, +0)); //false console.log(-0 === +0); //true複製代碼
Object.is 和 ==
差得遠了, ==
在類型不一樣時,須要進行類型轉換,前文已經詳細說明。
本文轉載自 寒冬求職季之你必需要懂的原生JS