想要成为一个专业的前端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);复制代码
若有错误或者缺漏,欢迎指点。