(1)typeof 和 instanceof 1、typeof 對於基本數據類型(boolean、null、undefined、number、string、symbol)來說,除了 null 都可以顯示正確的類型;對於對象來說,除了函數都會顯示 object。 2、instanceof 是通過原型鏈 ...
(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[1]);
}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、迴圈實現
這裡是淺拷貝,因為對象還是拷貝的地址。
var a = {
name:'coco',
age:33,
fn:function(){
console.log("111")
},
children:{
age:66
}
};
var b = {};
for(var i in a){
b[i] = a[i];
}
b.children.age = 100;
console.log(a);
console.log(b);
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、遞歸
var a = {
name:'kiki',
age:25,
children:{
name:'CoCo',
age:12
},
arr:['111','222'],
fn:function(){
console.log(1);
}
};
function copy(obj){
if(obj instanceof Array){
var newObj = [];
}else{
var newObj = {};
}
for(var i in obj){
if(typeof obj[i] == 'object'){
newObj[i] = copy(obj[i]);
}else{
newObj[i] = obj[i];
}
}
return newObj;
}
var b = copy(a);
console.log(a);
console.log(b);
c、jQuery.extend()
jQuery.extend([deep],target,object1,object2...),deep位Boolean類型,如果為true則進行深拷貝。