前端整理——javaScript部分
(1)typeof 和 instanceof
1、typeof 對於基本資料型別(boolean、null、undefined、number、string、symbol)來說,除了 null 都可以顯示正確的型別;對於物件來說,除了函式都會顯示 object。
2、instanceof 是通過原型鏈來判斷的。可以判斷一個物件的正確型別,但是對於基本資料型別的無法判斷。
3、相關筆試題:
1)JavaScript中如何檢測一個變數是一個String型別?請寫出函式實現。
var str = 'ssssssss'; typeof str === 'string' str.constructor === String
(2)閉包
1、閉包是指有權訪問另一個函式作用域中的變數的函式。
建立閉包的常見方式,就是在一個函式內部建立另一個函式。
function a(){
var num = 100;
function b(){
console.log(num);
}
return b;
}
var c = a();
c(); //100
如上所示:函式b可以訪問到函式a中的變數,函式b就是閉包。
2、閉包的用途:1)讀取函式內部的變數;2)讓這些變數始終儲存在記憶體中
由於閉包會攜帶包含它的函式的作用域,因此會比其他函式佔用更多的記憶體。過度使用閉包可能會導致記憶體佔用過多,建議只在絕對必要時再考慮使用閉包。
3、相關面試題
1)迴圈中使用閉包解決‘var’定義函式的問題
for(var i = 1; i <= 5; i++){
setTimeout(function timer(){
console.log(i);
},2000)
}
這裡因為setTimeout是非同步執行,迴圈會先執行完畢,這時i等於6,然後就會輸出5個6。解決方法如下所示:
第一種是使用let
for(let i = 1; i <= 5; i++){ setTimeout(function timer(){ console.log(i); },2000) }
第二種是使用閉包
for(var i = 1; i <= 5; i++){
(function(j){
setTimeout(function timer(){
console.log(j);
},2000)
})(i)
}
這裡首先使用了立即執行函式將i傳入函式內部,這個時候值就被固定在了引數j上面不會改變,當下次執行timer這個閉包的時候,就可以使用外部函式的變數j。
(3)原型和原型鏈
1、我們建立的每一個函式都有一個prototype(原型)屬性,這個屬性是一個指標,指向一個物件。而這個物件的用途是包含可以由特定型別的所有例項共享的屬性和方法。prototype就是通過調動建構函式而建立的那個物件例項的原型物件。
使用原型物件的好處是可以讓所有物件例項共享它所包含的屬性和方法。
示例:
function Person(){}
var p1 = new Person();
var p2 = new Person();
Person.prototype.name = 'CoCo';
console.log(p1.name); //CoCo
console.log(p2.name); //CoCo
Person建構函式下有一個prototype屬性。Person.prototype就是原型物件,也就是例項p1、p2的原型。
2、在預設情況下,所有原型物件都會自動獲得一個constructor屬性,這個屬性包含一個指向prototype屬性所在函式的指標。
function Person(){}
console.log(Person.prototype.constructor === Person); //true
3、Firefox、Safari和Chrome在每個物件上都支援一個屬性__proto__,這個屬性對指令碼是完全不可見的。__proto__用於將例項與建構函式的原型物件相連。
連線例項與建構函式的原型物件之間。
function Person(){}
var p1 = new Person();
console.log(p1.__proto__ === Person.prototype); //true
//isPrototypeOf:檢測一個物件是否是另一個物件的原型。或者說一個物件是否被包含在另一個物件的原型鏈中
console.log(Person.prototype.isPrototypeOf(p1)); //true
//getPrototypeOf:返回物件__proto__指向的原型prototype
console.log(Object.getPrototypeOf(p1) == Person.prototype); //true
(4)call、apply、bind
每個函式都包含兩個非繼承而來的方法: appy()和call()。這兩個方法的用途都是在特定的作用域中呼叫函式,然後可以設定呼叫函式的this指向。
1、apply()第一個引數是this所要指向的那個物件,如果設為null或undefined或者this,則等同於指定全域性物件。二是引數(可以是陣列也可以是arguments物件)。
var a = 1;
var obj1 = {
a:2,
fn:function(){
console.log(this.a)
}
};
obj1.fn(); //2
obj1.fn.apply(this); //1
2、call()方法可以傳遞兩個引數。第一個引數是指定函式內部中this的指向(也就是函式執行時所在的作用域),第二個引數是函式呼叫時需要傳遞的引數。第二個引數必須一個個新增。
3、bind()方法可以傳遞兩個引數。第一個引數是指定函式內部中this的指向,第二個引數是函式呼叫時需要傳遞的引數。第二個引數必須一個個新增。
4、三者區別:
1)都可以在函式呼叫時傳遞引數,call、bind方法需要直接傳入,而apply方法可以以陣列或者arguments的形式傳入。
2)call、bind方法是在呼叫之後立即執行函式,而bind方法沒有立即執行,需要將函式再執行一遍。
5、手寫三種函式
1)apply
Function.prototype.myApply = function(context){
if(typeof this !== 'function'){
throw new TypeError('Error');
}
context = context || window;
context.fn = this;
let result;
if(arguments[1]){
result = context.fn(...arguments);
}else{
result = context.fn();
}
delete context.fn;
return result;
};
2)call
Function.prototype.myCall = function(context){
if(typeof this !== 'function'){
throw new TypeError('Error');
}
context = context || window;
context.fn = this;
const args = [...arguments].slice(1);
const result = context.fn(...args);
delete context.fn;
return result;
};
3)bind
Function.prototype.myBind = function(context){
if(typeof this !== 'function'){
throw new TypeError('Error');
}
const _this = this;
const args = [...arguments].slice(1);
return function F(){
if(this instanceof F){
return new _this(...args,...arguments);
}
return _this.apply(context,args.concat(...arguments));
}
};
bind返回了一個函式,對於函式來說有兩種方式呼叫,一種是直接呼叫,一種是通過new的方式:
直接呼叫,這裡選擇了apply的方式實現,因為因為 bind 可以實現類似這樣的程式碼 f.bind(obj, 1)(2),所以我們需要將兩邊的引數拼接起來,於是就有了這樣的實現 args.concat(...arguments)。
最後來說通過 new 的方式,對於 new 的情況來說,不會被任何方式改變 this,所以對於這種情況我們需要忽略傳入的 this
(5)深拷貝淺拷貝
1、淺拷貝
let a = {
age: 1
}
let b = a
a.age = 2
console.log(b.age) // 2
物件型別在賦值的過程中其實是複製了地址,從而會導致改變了一方其他也都被改變的情況。通常在開發中我們不希望出現這樣的問題,我們可以使用淺拷貝來解決這個情況。
1)概念:對於物件型別,淺拷貝就是對物件地址的拷貝,拷貝的結果是兩個物件指向同一個地址,並沒有開闢新的棧,修改其中一個物件的屬性,另一個物件的屬性也會改變。
2)實現:
a、通過Object.assign來實現。Object.assign()方法的第一個引數是目標物件,後面的引數都是源物件。用於物件的合併,將源物件的所有可列舉屬性複製到目標物件。
Object.assign 只會拷貝所有的屬性值到新的物件中,如果屬性值是物件的話,拷貝的是地址,所以並不是深拷貝。
let a = {
age: 1
}
let b = Object.assign({}, a)
a.age = 2
console.log(b.age) // 1
b、通過展開運算子 ... 來實現淺拷貝
let a = {
age: 1
}
let b = { ...a }
a.age = 2
console.log(b.age) // 1
c、迴圈實現
let a = {
age: 1
}
let b = { ...a }
a.age = 2
console.log(b.age) // 1
2、深拷貝
let a = {
age: 1,
jobs: {
first: 'FE'
}
}
let b = { ...a }
a.jobs.first = 'native'
console.log(b.jobs.first) // native
淺拷貝只解決了第一層的問題,如果接下去的值中還有物件的話,那麼就又回到最開始的話題了,兩者享有相同的地址。要解決這個問題,我們就得使用深拷貝了。
1)概念:對於物件型別,深拷貝會開闢新的棧,兩個物件對應兩個不同的地址,修改其中一個物件的屬性,另一個物件的屬性不會改變。
2)原理:深複製--->實現原理,先新建一個空物件,記憶體中新開闢一塊地址,把被複制物件的所有可列舉的(注意可列舉的物件)屬性方法一一複製過來,注意要用遞迴來複制子物件裡面的所有屬性和方法,直到子子.....屬性為基本資料型別。
3)實現:
a、通過 JSON.parse(JSON.stringify(object)) 來解決
缺點:可以滿足基本的深拷貝,但是對於正則表示式、函式型別則無法進行拷貝。它還會拋棄物件的constructor。
let a = {
age: 1,
jobs: {
first: 'FE'
}
}
let b = JSON.parse(JSON.stringify(a))
a.jobs.first = 'native'
console.log(b.jobs.first) // FE
b、jQuery.extend()
jQuery.extend([deep],target,object1,object2...),deep位Boolean型別,如果為true則進行深拷貝。