想要成爲一個專業的前端er,學習JavaScript是一條必經之路。曾經的我是一個前端新手時,只會寫點html+css,可是不敢寫JavaScript,以爲這個太難了,看到它就懼怕,可是後來仍是硬着寫,寫了一段時間之後感受JavaScript寫着還行,逐漸地也就喜歡上這門語言了。接下來我根據我本身學過的JavaScript技術寫點學習總結,大致內容是函數做用域,閉包,this指向,原型鏈,ES6經常使用問題等等。
<script>
//全局做用域
function add(a){
//add函數做用域
console.log(a + b);
}
var b = 2;
add(1);
</script>複製代碼
上述代碼執行 add()後會再執行console.log()這句代碼,但括號裏有a和b兩個變量相加,js引擎就會先在add()函數做用域內尋找a和b兩個變量,此時a做爲函數的實參被找到並被賦值到了括號裏的a變量,但此時還未找到b變量的值,這個時候引擎就會往外層嵌套的做用域裏去尋找b這個變量,直到引擎在最外層做用域(全局做用域)還未找到b變量時就會拋出Uncaught ReferenceError: b is not defined(引用錯誤,b變量未定義)。javascript
//通常咱們定義變量是先聲明再賦值使用的
var a;
a = 1;
console.log(a); //此時會打印出a的值爲1
//但還有另外一種寫法,獲得的結果也相同
a = 1;
console.log(a); //這時也會打印出1
var a;複製代碼
另外一種寫法就是變量提高的一個案例體現,因爲JavaScript是沒有編譯階段的,它是邊解釋邊執行的,因此它會有一個預解釋的過程,函數聲明和變量聲明每次會被解釋器提到方法體的最頂層,這也是聲明提高的概念。接下來再看另外一個案例:css
//初始化a和b
var a = 1;
var b = 2;
console.log(a,b); //這裏會打印出a和b的值也就是1,2
//再看看另外一個寫法
var a = 1;
console.log(a,b); //這裏會打印出1,undefined
var b = 2;複製代碼
產生上面代碼兩種結果的緣由實際上是由於 var b
被提高了,可是初始化的var b=2
並無被提高,這說明在js裏只有聲明的變量纔會被提高,初始化的不會。變量提高後的代碼以下:html
//因爲b的值初始化時undefined, js也是按照上下文執行的,因此此時打印b結果纔是undefined
var a = 1;
var b;
console.log(a,b);
b = 2;複製代碼
add();
function add(){
console.log(1);
}複製代碼
函數提高與變量提高是同樣的,定義完add函數之後,它會被提高到最頂層,而後add就能夠調用到了, 但有的寫法不行。前端
add();
var add = function(){
console.log(1);
}複製代碼
此時控制檯會打印出Uncaught TypeError: add is not a function(類型錯誤:add不是一個函數),由於這個時候觸發的是變量提高,var add
被提高到了最頂層,它的初始化值也就是undefined,因此纔會報錯。java
var a;
function a(){}
console.log(a); //打印出function a()複製代碼
聲明提高的順序是變量聲明優先於函數聲明,可是函數的聲明會覆蓋未定義的同名變量,再看另外一個例子:es6
//例子
var a = 1;
function a(){}
console.log(a); //打印出1
//例子等價於下面代碼
var a;
function a(){}
a = 1;
console.log(a);複製代碼
var a
上去,可是不管前面是什麼,後面的函數聲明都能將其覆蓋。再看看一個筆試題數組
console.log(a);
var a = 1;
function foo(){
console.log(a);
var a = 2;
console.log(a);
}
foo();
console.log(a);
//打印順序結果是 undefined undefined 2 1 複製代碼
把上面的代碼經過變量提高之後:瀏覽器
var a;
console.log(a); //第一處打印
a = 1;
function foo(){
var a;
console.log(a); //第二處打印
a = 2;
console.log(a); //第三處打印
}
foo();
console.log(a); //第四處打印複製代碼
js由全局環境開始執行,以下圖當全局環境執行到fn1(50)時,此時就會去執行fn1的環境,而後fn1執行到fn2(20)和fn(30)時就會再去執行fn2的環境,直到fn2()被執行完成。緩存
var a = 1;
var b = 2;
function fn1(c){
var a = 10;
function fn2(c){
var a = 100;
b = a + c;
console.log(b);
}
fn2(20);
fn2(30);
}
fn1(50);複製代碼
每一個執行環境中都有一個對應的變量對象,它把環境中定義的變量和函數都保存在這個對象裏。bash
紅寶書 上對於閉包的定義:閉包是指有權訪問另一個函數做用域中的變量的函數
MDN 對閉包的定義爲:閉包是指那些可以訪問自由變量的函數。 (其中自由變量,指在函數中使用的,但既不是函數參數arguments也不是函數的局部變量的變量,其實就是另一個函數做用域中的變量。)
var a = 1;
function fn1(){
var b = 2;
function fn2(){
console.log(b);
}
return fn2;
}
var fn = fn1();
fn();
// console.log(b) 這裏會打印出 b is not defined複製代碼
先來理一下上面代碼的做用域,上面函數分爲全局做用域、fn1做用域、fn2做用域。在全局做用域下訪問b變量,因b變量是在f1做用域內部定義的,js的查找機制也是從裏到外的,故b變量沒法找到,因此纔會打印出b變量未定義。可是在fn2做用域下訪問b變量,fn2就會先在fn2做用域下查找b變量,若是未找到就會一直往外層做用域查找有沒有b變量也就是會在最近的fn1做用域下找到b變量而後直接賦給fn2的b。
這樣看,閉包就是fn2能訪問到其餘外層做用域的變量,可是外層做用域不能直接訪問到內部做用域的變量,也能夠理解爲定義在一個函數內部的函數,閉包的本質是函數內部和函數外部之間鏈接的一條橋樑。
//閉包用做計數器
//被用做讀取函數內部的變量,這些變量始終被存在內存裏
function sum(){
var n = 0;
function inc(){
return n++;
}
return inc;
}
var inc2 = sum();
console.log(inc2()); //打印出 0
console.log(inc2()); //打印出 1
console.log(inc2()); //打印出 2
inc2 = null; //釋放該內存複製代碼
計數器的閉包函數被建立之後,將sum返回的inc函數給了inc2,而後返回的n變量存在inc2的內存塊內,三次打印inc2()的值就是調用了三次 n++
,故三次打印依次打印出了0、一、2,最後再將inc2的內存塊釋放掉,由於閉包使得函數裏的變量始終存在內存裏,內存會佔不少消耗,最終會形成瀏覽器性能問題也就是內存溢出問題。
//建立私有變量和私有函數
function student(name){
var age;
function setAge(a){
age = a;
}
function getAge(){
return age;
}
return{
name:name,
setAge: setAge,
getAge: getAge
}
}
var s = student('cc'); // name:'cc'
s.setAge(20); // age: 20
console.log(s.name,s.getAge()); //打印出 cc, 20
s = null;複製代碼
在student函數裏建立了一個私有變量age,使用私有函數setAge去間接給age賦值,使用私有函數getAge返回age的值。
function sum(){
var arr = [];
for(var i = 0; i< 10; i++){
arr[i] = function(){
return i;
}
}
return arr;
}
var sum2 = sum();
console.log(sum2[0]()); //打印出 10複製代碼
這個時候就會疑惑爲何打印不是0,實際上是由於sum[0]返回的是一個函數,根據查找機制函數返回的i返回的是循環結束之後i++的值也就是10,那麼把代碼修改一下改爲想要的結果:
//es6 將var改爲let
function sum(){
var arr = [];
for(let i = 0; i< 10; i++){
arr[i] = function(){
return i;
}
}
return arr;
}
var sum2 = sum();
console.log(sum2[0]()); //打印出 0
//自執行函數
function sum(){
var arr = [];
for(let i = 0; i< 10; i++){
arr[i] = (function(n){
return function(){
return n;
}
})(i)
}
return arr;
}
var sum2 = sum();
console.log(sum2[0]()); //打印出 0複製代碼
第一個例子裏ES6的let與var區別後面再說,再看第二個例子自執行函數將每次循環的i值當作實參賦給了形參n,而後再將n返回出去,最後就獲得了每次循環的i值。
function sum(){
var n = 0;
function inc(){
return n++;
}
return inc;
}
//這裏我將sum()實例化後的inc2註釋掉,用用自執行函數
//var inc2 = sum();
console.log(sum()()); //打印0
console.log(sum()()); //打印0
console.log(sum()()); //打印0複製代碼
這裏使用了三次自執行函數,打印仍是0的緣由實際上是每次調用sum()()
都是獨自生成了一個內存塊,調用了三次也就是生成了三個不一樣的內存塊存儲n值。
//函數求和,可是每次執行完之後保存每次放進入的數字
//至關於於Object的key和value
//例如:var abc = { "1,2,3" : 6 }
var save = function(){
var obj={}
function fn(){
var sum = 0;
for(var i =0;i<arguments.length;i++){
sum = sum + arguments[i];
}
return sum;
}
return function(){
//將arguments強轉換成數組而後執行Array.join()方法
var arg = Array.prototype.join.call(arguments, ',');
obj[arg] = fn.apply(null, arguments);
console.log(obj);
for(var i = 0;i<Object.keys(obj).length;i++){
console.log(Object.keys(obj)[i].split(',').map(Number));
}
}
}();
save(1,2,3,4,5);
save(1,2,3,4,5,6,7);
save();複製代碼
運行結果
能夠看到每次調用save()函數之後,每次存進去的參數都會被保存在obj集合裏,obj集合的key就是每次保存進去的全部參數,obj集合的value值就是每次傳入參數後計算後的總和值。
this === window; //true
'use strict';
this === window; //true
this.n = 10;
console.log(this.n); //打印出10複製代碼
非嚴格模式和嚴格模式下this都指向的是最頂層做用域(瀏覽器是window)
//非嚴格模式下
var n = 10;
function fn(){
console.log(this); //打印出window
console.log(this.n); //打印出10
}
fn();
//嚴格模式下
'use strict';
var n = 10;
function fn(){
console.log(this); //打印出undefined
console.log(this.n); //報錯TypeError
}複製代碼
非嚴格模式下函數裏的this指向window ,this.n則能夠打印出10。但嚴格模式下this則指向undefined,故打印this.n時瀏覽器會打印出TypeError:Cannot read property 'n' of undefined(沒法讀取到未定義的屬性n)。
var n = 1;
function fn(){
console.log(this.n);
}
var obj = {
n:5,
fn:fn,
obj2:{
n:10,
fn:fn
}
}
obj.fn(); //打印5
obj.obj2.fn(); //打印10複製代碼
var n = 1;
function fn(){
console.log(this.n);
}
var obj = {
n:5,
fn:fn
}
var fn2 = obj.fn;
fn2(); //此時打印的是1 而不是5複製代碼
將obj.fn賦值給了fn2,因爲fn2是在window指向下的,故fn2()去調用fn()函數時this指向從obj內部指向了window。
var n = 1;
function fn(){
console.log(this.n);
}
var obj = {
n:5,
fn:fn
}
setTimeout(obj.fn,1000) //一秒後打印出1複製代碼
內置函數setTimeout和setInterval這種,第一個參數回調函數裏的this默認指向的是window。
var n = 1;
var obj = {
n:5,
fn:()=>{
console.log(this.n);
}
}
obj.fn(); //打印出1複製代碼
ES6的箭頭函數與普通函數不一樣,箭頭函數中沒有this指向,它必須經過查找做用域鏈來決定this的值,若是箭頭函數包含在一個普通函數裏,則它的this值會是最近的一個普通函數的this值,不然this的值會被設置成全局變量也就是window。
var n = 1;
function fn(){
console.log(this.n);
}
var obj = {
n:10
}
fn(); //打印出1
fn.call(obj); //打印出10
fn.apply(obj); //10
fn.bind(obj)(); //10複製代碼
call、apply、bind方法的第一個參數是this指向的目標,它會強制改變this的指向,而且使得this不能再被改變。
var n = 1;
function Fn(n){
this.n = n;
console.log(this); //打印出 Fn: { n:10 }
//return {}
//return function f(){}
}
var fn = new Fn(10);複製代碼
new操做符調用時this會指向生成的新對象,可是new調用的返回值沒有顯示返回對象或者函數,纔是返回生成的新對象。
function Foo(name){
this.name = name;
}
var foo = new Foo('CC');
console.log(foo.constructor === Foo); //true
console.log(foo.__proto__ === Foo.prototype); //true
console.log(Foo.prototype.constructor === Foo); //true複製代碼
Foo()函數被new實例後成爲實例對象foo之後,foo實例對象裏產生了構造函數constructor和__proto__原型屬性,foo的constructor指向Foo自己,控制檯打印foo.constructor會把Foo這個函數自身顯示出來。而foo的原型屬性_proto__則指向了Foo的原型對象屬性prototype,固然Foo的原型對象屬性prototype的構造函數constructor是指向了Foo自身。
__proto__和prototype看起來很類似,可是二者仍是有點區別的,__proto__存在於全部的對象上,prototype存在於全部的函數上,從上面例子能夠看到foo是一個實例對象因此它只擁有__proto__屬性,但沒有prototype屬性,嘗試去打印foo.prototype能夠看到結果是undefined,可是在js裏函數也是對象的一種,因此在Foo裏__proto__屬性和prototype屬性都會同時擁有。
JavaScript經過 __proto__屬性指向父類對象,直到指向Object對象爲止,這樣造成了一個原型的鏈條就叫作原型鏈,原型鏈的盡頭也就是Object.prototype,由於再往下指就是null了。
//ES6 class語法
class Square{
constructor(edge){
this.edge = edge;
}
getEdge(){
console.log(`正方形的邊長是${this.edge}`);
}
}
new Square(5).getEdge(); //打印出 正方形的邊長是5
//用原型鏈模擬實現class語法
var Square = (function (){
function Square(edge){
this.edge = edge;
}
Square.prototype.getEdge = function(){
console.log(`正方向的邊長是${this.edge}`);
}
return Square;
})();
new Square(3).getEdge(); //打印出 正方形的邊長是3複製代碼
由上面能夠看出ES6的class其實就是構造器的語法糖,在class裏定義的函數其實就是放在了構造器的prototype裏。
//ES6的class繼承
class Person{
constructor(name){
this.name = name;
}
}
class Student extends Person{
constructor(name, number){
super(name);
this.number = number;
}
getView(){
console.log(`學生姓名是${this.name},學號是${this.number}`);
}
}
new Student('小米',20200101).getView(); //打印出 學生姓名是小米,學號是20200101
//使用原型繼承和組合繼承模擬ES6的繼承
//原型繼承
function inheritsLoose(child,parent){
child.prototype = Object.create(parent.prototype);
child.prototype.constructor = child;
child.__proto__ = parent;
}
var Person = function(name){
this.name = name;
}
var Student = (function(_Person){
inheritsLoose(Student,_Person);
function Student(name,number){
var _this;
//組合繼承
_this = _Person.call(this,name) || this;
_this.number = number;
return _this;
}
Student.prototype.getView = function(){
console.log(`學生姓名是${this.name}, 學號是${this.number}`);
}
return Student;
})(Person);
new Student('小米',20200101).getView(); //打印出 學生姓名是小米,學號是20200101複製代碼
ES6的繼承機制其實就是實現了原型繼承和組合繼承,子類構造器調用父類構造器並將this指向了子類構造器。
ES6裏引入了一個塊級做用域,它存在於函數內部或者{ }中,接着就有了塊級聲明,塊級聲明用來聲明在指定塊的做用域外沒法訪問的變量。
ES6裏用let和const來當作塊級聲明去聲明變量,爲的就是控制變量的生命週期,用var聲明時會出現不少問題,好比變量提高,能重複聲明變量等,可是用let和const時就不會再產生該問題了。
//不存在變量提高
let a;
console.log(a); //打印出 undefined
a = 1;
//不能重複聲明
let b;
let b = 2;
console.log(b); //打印出SyntaxError錯誤,b變量已經被聲明瞭
//不存在污染變量
for(let i = 0;i<3;i++){
//dosth
}
console.log(i); //打印出ReferenceError錯誤, i變量未定義
//不綁定全局做用域
let c = 1;
function fn(){
console.log(this.c); //打印出 undefined
}
fn()複製代碼
接下來再來看看let和const的區別,const用於定義常量,定義結束之後不容許被修改,不然會報TypeError的錯誤,雖然const定義後不能被修改其值,但容許被修改內部的值,例如當用const定義一個object類型時:
const obj = { a:1 };
obj.a = 2;
obj.b = 3;
console.log(obj); //打印出 { a:2, c:3 }複製代碼
var m = 1, n = 2;
function fn(){
console.log(this.m, this.n);
}
var obj = {
m : 5,
n : 10
}
fn(); //打印 1,2
fn.call(obj, m, n); //打印 5,10
fn.apply(obj,[m,n]); //打印 5,10
fn.bind(obj,m,n)(); //打印 5,10複製代碼
從上面例子裏很容易就能夠看到三者的共同點都能改變this指向,接下來再說三者的區別:
//將數組扁平化後去重並按數字大小從大到小排序最後再留下小於50的數字
var a = [49,[12,14,25,7],[23,53,25,[98,9,[65,25,20]]],65,20,9];複製代碼
//用flat()實現扁平化 上面最深的嵌套有3層
let b = Array.from(new Set(a.flat(3))).sort((a,b)=> b - a).filter(i => i < 50);
//用ES6的generator函數實現扁平化
function* flatUp(array){
for(let item of array){
if( Array.isArray(item) ){
yield* flatUp(item);
}else{
yield item;
}
}
}
let b = Array.from(new Set([...flatUp(a)])).sort((a,b)=> b - a).filter(i => i < 50);複製代碼
首先得理解堆內存和棧內存的區別:
基本數據類型(如number,String類型)都會直接存儲在棧內存裏,但引用數據類型(如Object,Array類型)在棧內存中存儲的是指針位置,實際真實數據存儲在堆內存裏,該指針指向堆存儲的該實體的起始地址。
深拷貝和淺拷貝都是針對引用數據類型(Object,Array)的方案
深淺拷貝的區別:淺拷貝是複製指向對象的指針,而不復制整個對象的自己,新舊對象使用的是同一個內存塊。可是深拷貝會建立一個與原來如出一轍的對象,而且不共用同一個內存塊,修改新對象時不會改到原對象。
先來看看淺拷貝和普通直接賦值的區別:
//直接賦值
var obj1 = {
n: 2,
arr:[1,[2,3]]
}
var obj2 = obj1;
obj2.n = 1;
obj2.arr[1] = [5,6,7];
console.log(obj1); //obj1: { n:1, arr:[1,[5,6,7]] }
console.log(obj2); //obj2: { n:1, arr:[1,[5,6,7]] }
//淺拷貝
function shallowCopy(obj){
var data = {}
for(let item in obj){
if(obj.hasOwnProperty(item)){
data[item] = obj[item];
}
}
return data;
}
var obj1 = {
n: 2,
arr:[1,[2,3]]
}
var obj3 = shallowCopy(obj1);
obj3.n = 1;
obj3.arr[1] = [5,6,7];
console.log(obj1); //obj1: { n:2, arr:[1,[5,6,7]] }
console.log(obj3); //obj3: { n:1, arr:[1,[5,6,7]] }複製代碼
//多重嵌套時Object.assign()是淺拷貝
var obj = { a:{a:1,b:2}};
var obj2 = Object.assign({},obj);
obj2.a.a = 10;
obj2.a.c = 5;
console.log(obj); //{a:{a:10,b:2,c:10}}
console.log(obj2); //{a:{a:10,b:2,c:10}}
//當Object只有一層時Object.assign()是深拷貝
var obj = { a:1 };
var obj2 = Object.assign({},obj);
obj2.a = 10;
console.log(obj); //{a:1}
console.log(obj2); //{a:10}複製代碼
2. Array.slice()
var arr = [1,2,{a:1}];
var arr2 = arr.slice();
arr2[1] = 5;
arr2[2].a = 5;
console.log(arr); // [1,2,{a:5}]
console.log(arr2); // [1,5,{a:5}]複製代碼
var arr = [1,2,{a:1}];
var arr2 = arr.concat();
arr2[1] = 10;
arr2[2].a = 10;
console.log(arr); // [1,2,{a:10}]
console.log(arr2); // [1,10,{a:10}]複製代碼
//這種方法只能用來深拷貝數組或者對象,不能用於拷貝函數
var arr = [1,2,{a:1}];
var arr2 = JSON.parse(JSON.stringify(arr));
arr2[1]=10;
arr2[2].a=10;
console.log(arr); // [1,2,{a:1}]
console.log(arr2); // [1,10,{a:10}]複製代碼
2. 遞歸
function deepClone(obj){
let result = typeof obj === 'function' ? [] : {};
if(obj && typeof obj === 'object'){
for(let i in obj){
if(obj[i] && typeof obj[i] === 'object'){
result[i] = deepClone(obj[i]);
}else{
result[i] = obj[i];
}
}
return result;
}
return obj;
}複製代碼
//lodash函數庫使用 _.cloneDeep()
const _ = require('lodash');
var obj = {
a:1,
b:{c:2}
}
var obj2 = _.cloneDeep(obj);複製代碼
若有錯誤或者缺漏,歡迎指點。