栈内存与堆内存
栈内存:作用域
1.提供一个供JS代码自上而下执行的环境(代码都是在栈中执行的) 2.由于基本数据类型值比较简单,他们都是直接在栈内存中开辟一个位置,把值直接存储进去的
当栈内存被销毁,存储的基础值也随之销毁
堆内存:引用值对应的空间
1.存储引用类型值的
- 对象:存储的是键值对
- 函数:代码字符串
当前堆内存释放销毁,那么这个引用值彻底没了
堆内存的释放
当堆内存没有被任何的变量或者其它东西所占用,浏览器会在空闲的时候,自主的进行内存回收,把所有不被占用的堆内存销毁(谷歌浏览器)[IE浏览器通过计数器来进行内存释放]
销毁方法: xxx=null
通过空对象指针null可以让原始变量(或者其它东西)谁都不指向,那么原有被占用的堆内存就没有被东西占用了,浏览器会销毁它
ES5变量提升机制
定义
:当栈内存(作用域)形成之后,JS代码自上而下执行之前,浏览器首先会把所有带var(声明)
和function(声明并定义)
关键词的进行提前的声明
或者定义
,这种预先处理机制称为"变量提升"
- 声明(declare): var a/function sum (默认值undefined)
- 定义(defined):a=12 (定义其实就是赋值操作)
变量提升细节知识点
1.
变量提升只发生在当前作用域
(例如:开始加载页面的时候只对全局作用域下的变量进行提升,因为此时函数中存储的都是字符串而已)
2.在全局作用域下声明的函数或者变量是"全局变量",同理,在私有作用域下声明的变量是"私有变量"[带VAR/FUNCTION的才是声明]
3.浏览器很懒,做过的事情**
不会重复
**执行第二遍,也就是,当代码执行遇到创建函数这部分代码后,直接的跳过即可(因为在提升阶段就已经完成函数的赋值操作了)
私有作用域形成后,也不是立即执行代码,而是先进行变量提升(变量提升前,先进行形参赋值)
在ES3/ES5语法规范中,只有全局作用域和函数执行的私有作用域(栈内存),其它大括号不会形成栈内存
知识点补充:逻辑与&&和逻辑或||
优先级:逻辑与的优先级高于逻辑或
console.log(0||1&&2||0||3&&2||1);//=>2
逻辑与
A&&B,验证A的真假,为真结果是B,为假结果为A
var b=1&&2//=>b=2
逻辑与的实战
//=>只有当传递的值是函数的时候我们才让其执行
~function (caller){
caller&&caller()
}(function (){console.log('ok');});
逻辑或
A||B,验证A的真假,为真结果为A,为假结果为B
var a=1||2//=>a=1
逻辑或的实战
//=>"给形参赋值默认值:验证传递的参数值,如果没有传递实参,赋值为0"
function fn(x){
x=x||0;
//=>如果x没有传递值,x=undefined=>x=undefined||0
}
特殊情况
在全局作用域下声明一个变量,也相当于给window全局对象设置了一个属性,变量的值就是属性值(私有作用域中声明的私有变量和window没啥关系)
//=>1.先进行变量提升 var a;此时同时也给window全局设置了一个属性名为a,属性值为undefined的属性
console.log(a);//=>undefined
console.log(window.a)//=>undefined
console.log('a' in window)//=>true in:检测某个属性是否隶属于这个对象
var a=12;
console.log(a);//=>全局变量a 12
console.log(window.a);//=>window的一个属性名a 12
a=13;
console.log(window.a);//=>13
window.a=14;
console.log(a);//=>14
//=>全局变量和window中的属性存在“映射机制”,给变量重新赋值时,window下对应的属性值也会发生改变;改变属性值时,对应的变量也会被重新赋值
形参与arguments的映射机制
1、在JS非严格模式下,函数中的形参变量和arguments存在映射机制
2、arguments和形参之间的映射是以arguments的索引为基础完成的,arguments中有这个索引,浏览器会完成和对应形参变量中的映射机制搭建,如果形参比arguments中的个数多,那么多出来的形参是无法和arguments中的对应的索引建立关联的
3、argument和形参的映射机制建立在函数执行后形参赋值的一瞬间,此时能建立映射机制的建立映射机制,不能建立起来的,以后不管怎么操作都无法再建立了
~function fn(x,y){
var arg=arguments;
arg[0]=100;
console.log(x);//=>100
console.log(arg[1]);//=>undefined
y=200;
console.log(arg[1]);//=>undefined
console.log(y);//=>200
arg[1]=300;
console.log(y);//=>200
}(10);
在严格模式下不支持使用 arguments.callee(函数本身)/arguments.callee.caller(函数执行的suzh),而且不存在映射机制
~function(x,y){
'use strict'
var arg=arguments;
arg[0]=100;
console.log(x);//=>10
x=200;
console.log(arg[0]);//=>100
console.log(arg.callee.caller)//=>Uncaught TypeError: 'caller', 'callee', and 'arguments' properties may not be accessed on strict mode functions or the arguments objects for calls to them
}(10);
在ES6新语法规范中可以直接给形参设置默认值
function fn(x=0){
//=>如果x没有传递值,默认值是零,一旦传递值,不管传递的是啥,都是按照传递的值处理的
console.log(x);
}
fn();//=>0
fn(null);//=>null
fn(undefined);//=>传递undefined,浏览器也是按照没有传递值处理的
全局作用域下带VAR和不带VAR的区别
带var表示声明变量 不带var表示给window对象下添加属性
//=>变量提升:无
console.log(a);//=>Uncaught ReferenceError: a is not defined 因无变量提升,也没有window.a这个属性,所以此时a相当于一个没有声明过的变量,所以会出现报错的情况
a=12;
console.log(a);
console.log(a)的查找机制
:先找有无声明过的变量a,如果没有再找有无window.a这个属性,如果还没有还是当做没有声明过的变量
语法补充
var a=12,
b=13;
此时相当于
var a=12;
var b=13;
var a=b=13;
相当于
var a=13;
b=13;//<=>window.b=13
var n=m=[12,23];
/*
* 1.开辟堆内存,存储键值对
* 2.var n
* 3.n=AAAFFF111
* m=AAAFFF111
/
相当于
var n=[12,23];
var m=[12,23];
私有作用域中带VAR和不带VAR的区别
1.带VAR的在私有作用域变量提升阶段,都声明为私有变量,和外界没有任何关系
2.不带VAR不是私有变量,会向它的上级作用域查找,看是否为上级变量,不是,继续向上查找,一直找到window为止(
我们把这种查找机制叫做:“作用域链”
),也就是我们在私有作用域中操作的这个非私有变量,是一直操作别人的
console.log(a, b);//=>undefined undefined
var a = 12;
var b=12;
function fn() {
console.log(a, b);//=>undefined 12(这个输出的是全局中变量b的值)
var a = b = 13;
/*var a=13; b=13;*/ //相当于把全局变量b被重新赋值为13
console.log(a, b);//=>13 13(这个输出的是全局中变量b的值)
}
fn();
console.log(a, b);//=>12 13(在私有作用域中已经将变量b的值改变为13)
扩展
function fn(){
console.log(b)//=>Uncaught ReferenceError: b is not defined
b=13;
console.log('b' in window);//=>true 在作用域链查找的过程中,如果找到WIN也没有这个变量,相当于给WIN设置了一个B(window.b=13)
console.log(b);//=>13 <=>console.log(window.b)
}
fn();
console.log(b);//=>13
作用域链查找的机制
:
- 1.如果往上级查找的过程中,上级有这个变量,那直接修改上级这个变量的值
- 2.如果往上级查找的过程中,一直找到window下都没有这个变量,那么就相当于给window这个对象添加一个属性
练习题
var n=0;
function a(){
var n=10;
function b(){
n++;
console.log(n);
}
b();
return b;
}
var c=a();
c();
console.log(n);
结果 11,12,0
只对等号左边进行变量提升
/*
* 变量提升:
* var fn; =>只对等号左边进行变量提升
* sum = AAAFFF111;
*/
sum();
fn();//=>Uncaught TypeError: fn is not a function
var fn=function (){//=>匿名函数之函数表达式
console.log(1);
}//=>代码执行到此处会把函数值赋值给FN
fn();
function sum(){
console.log(2);
}
给匿名函数设置名字其实没有实际的用途,因为在函数外面是无法获取这个函数名的 匿名函数起的名字只能在函数体内使用
var fn=function sum() {
console.log(sum)//=>函数体本身
};
fn();
console.log(sum)//=>Uncaught ReferenceError: sum is not defined
var fn=function sum() {
console.log(sum())//=>会陷入死循环,fn执行后输出sum执行的结果,也就是fn执行的结果,会不停的执行函数
};
fn();
条件判断下的变量提升
在当前作用域下,不管条件是否成立都要进行变量提升
- 带VAR的还是只声明
- 带function的在老版本浏览器渲染机制下,声明和定义都处理,但是为了迎合ES6中的块级作用域,新版浏览器对于
(在条件判断中的函数)
,不管条件是否成立,都只是先声明,没有定义,类似于VAR
/*
* 变量提升
* 无 * /
console.log(a);//=>undefined 变量a只声明未定义
if("a" in window){
var a=100;
}
console.log(a);//=>100 变量a的值为100
老版本浏览器下
不管条件是否成立,都会进行变量提升
- 带VAR的只
声明
- 带FUNCTION的
声明并定义
/
* 变量提升:无
/
f=function(){return true;};//=>发生修改变为f=....false
g=function(){return false;};
~function(){
//变量提升 g=AAAFFF111
if(g() && [] == ![]){//=>此时函数g已经声明及定义了,是私有变量,所以执行函数g()的结果是true
f=function(){return false};//=>非私有变量,往上找,相当于改了window.f=....false,
function g(){return true};
}
}()
console.log(f());//=>false
//相当于自执行函数:(function(){return false;})();,把该函数执行,返回值为false
console.log(g());//=>false
//相当于自执行函数:(function(){return false;})();,把该函数执行,返回值为false
新版浏览器下
不管条件是否成立,都进行变量提升
- 带VAR的只
声明
- 带FUNCTION的只
声明
f=function(){return true;};
g=function(){return false;};
~function(){
//=>变量提升:只声明了函数g,未定义(相当于var g;)
if(g() && [] == ![]){//=>Uncaught TypeError: g is not a function 此时的g是undefined)
f=function(){return false};
function g(){return true};
}
}()
console.log(f());
console.log(g());
特殊情况
条件中的function在变量提升阶段也是只声明,当代码执行,如果条件成立,进入条件后的第一件事情,就是把函数定义了,然后再执行判断体中的代码
console.log(a,fn);//=>undefined undefined
if(1===1){
console.log(a,fn)//=>undefined fn函数本身
//=>当条件成立,进入到判断体中(在ES6中它是一个块级作用域)第一件事情并不是代码执行,而是类似于变量提升一样,先把定义了(最开始的时候已经声明了),也就是判断体中代码执行之前,FN就已经赋值了
var a=12;
function fn(){
console.log('ok');
}
}
console.log(a,fn);//=> 12 fn函数本身
重名问题的处理
带VAR和FUNCTION关键字声明相同的名字,这种也算是重名了(其实是一个FN,只是存储值的类型不一样)
关于重名的处理:如果名字重复了,不会重新的声明,但是会重新的定义(重新赋值)[不管是变量提升还是代码执行阶段皆是如此]
/
* 变量提升:
* fn=aaafff111
* =aaafff222
* =aaafff333
* =aaafff444
/
fn();//=>4
function fn(){console.log(1);}
fn();//=>4
function fn(){console.log(2);}
fn();//=>4
var fn =100;
fn();//<=>100() Uncaught TypeError: fn is not a function
function fn(){console.log(3);}
fn();
function fn(){console.log(4);}
fn();
ES6
ES6变量
在ES6中基于let/const等方式创建变量或者函数,不存在变量提升机制
切断了全局变量和WINDOW属性的映射机制,但是ES6的全局作用域下也是有WINDOW属性,只是没有了映射机制
不带let或者const就和ES6没有关系
基于LET和基于VAR创建变量,在私有变量和私有作用域链机制上是一样的
现在项目中ES5 && ES6混合开发模式,如果当前变量是基于LET创建的,就按照ES6的新语法机制渲染,否则按照ES5的老语法机制渲染
console.log(a);//=>Uncaught ReferenceError: a is not defined
let a=12;
console.log(window.a);//=>undefined
console.log('a' in window)//=>false
console.log(a);//=>12
在相同作用域中,基于LET不能声明相同名字的变量(不管用什么方式在当前作用域下声明了变量(例如var,或者设置形参),再次使用let创建都会报错)
let a=12;
cosole.log(a);
let a=13;//=>Uncaught SyntaxError: Identifier 'a' has already been declared
console.log(a);
//代码不会执行,直接报错
console.log(a);
var a=12;
console.log(a);
let a=13;//=>Uncaught SyntaxError: Identifier 'a' has already been declared
console.log(a);
//代码不会执行,直接报错
let fn=(a)=>{//=>ES6箭头函数
let a=b=c=100;
};
fn();//=>3-LET.js:22 Uncaught SyntaxError: Identifier 'a' has already been declared
虽然没有变量提升机制,但是在当前作用域代码自上而下执行之前,浏览器会做一个重复性检测:自上而下查找当前作用域下所有变量,一旦发现有重复的,直接抛出异常,代码也不会在执行了(虽然没有把变量提前声明定义,浏览器已经记住了,当前作用于域有哪些变量)
b=12;
console.log(b);//=>12
console.log(window.b);//=>12
a=12;//=>Uncaught ReferenceError: a is not defined
//=>代码执行之前,浏览器会记住所有ES6下声明的变量并且记住,此时代码开始执行,执行到这一步的时候,浏览器知道有这个变量,但是还没有声明,所以出现a is not defined的错误
console.log(a);
let a=13;
console.log(a);
let a=10,
b=10;
let fn=function (){
console.log(a.b)//=>Uncaught ReferenceError: a is not defined
//=>在当前私有作用域下,浏览器会先记住该作用域下会声明一个a,输出a的时候啊还没有被声明,所以会报错a is not defined
let a=b=20;//<=>let a=20; b=20;(把全局中的b改为20)
console.log(a,b)
};
//=>箭头函数,也可以这样写 let fn=(a)=>{}
fn();
console.log(a,b);
ES6的暂时性死区
在原有浏览器渲染机制下,基于typeof等逻辑运算符检测一个未被声明过的变量,不会报错,返回undefined
如果当前变量是基于ES6语法,在没有声明这个变量的时候,使用typeof检测会直接报错,不会是undefined,解决了原有的JS死区
//=>ES5下
console.log(typeof a);//=>'undefined'
------------------------------
//=>ES6下
console.log(typeof a);//Uncaught ReferenceError:a is not defined
ES6箭头函数
语法
let fn=(x,y)=>{};
fn(10,20);
只有一个形参,我们可以省略小括号
let fn=x=>{};
fn(10);
如果函数体中只有一句操作,并且是return的,我们可以省略大括号
let fn=function (x,y){return x+y;};
let fn =(x=0,y=0)=>x+y;
let fn=x=>y=>x+y;
---------------
var fn=function fn(x){
return function(y){
return x+y;
};
};
箭头函数中没有arguments
let fn=(...arg)=>{
console.log(arguments);//=>Uncaught ReferenceError: arguments is not defined
console.log(arg);//=>可以使用剩余运算符代替,而且arg是一个数组
};
fn(10,20,30,40);
箭头函数中没有自己的执行主体(this),它的this都是默认继承上下文中的this,即使通过call方法改变this指向也是没用的
let obj={
fn:(function (){//=>自执行函数中的this是window
return function(){
console.log(this);
}
})()
};
obj.fn();//=>this:obj
//=>如何让obj.fn()执行输出的this是window
第一种解决方案
let obj={
fn:(function (){
let _this=this;
return function (){
console.log(_this);//=>this只是一个变量,不是私有的,找上级作用域中的
}
})()
};
第二种解决方案
let obj={
fn:(function (){
return ()=>{
console.log(this);
}
})()
}
//=>箭头函数中的this和执行前面有没有点没有关系,箭头函数中没有this,使用到的this都是直接找上下文中的this来使用
//=>暂时理解为找到上级作用域中的this
私有变量
在私有作用域中,只有以下两种情况是私有变量
- A:声明过的变量(带VAR/FUNCTION/LET)
- B:形参也是私有变量
函数执行的步骤
- 1、形参赋值
- 2、变量提升
- 3、代码自上而下执行
- 4、当前栈内存(私有作用域)销毁或者不销毁
/
* 变量提升
* var a; var b; var c;
/
var a=12,
b=13,
c=14;
function fn(a){
/
* 1.形参赋值 a=12
* 2.变量提升 var b;
/
console.log(a,b,c)//=>(12,undefined,14)
var b=c=a=20;
/
* var b=20;//=>私有变量b赋值为20
* c=20;//=>把全局中的c修改为20
* a=20;//=>私有变量a修改为20
/
console.log(a,b,c)//=>(20,20,20)
}
fn(a);//=>把FN执行(小括号中是实参:值) =>执行FN把全局变量A的值12当做实参传递给函数的形参 =>fn(12)
console.log(a,b,c);//=>(12,13,20)
练习
var ary=[12,23];
function fn(ary){
console.log(ary);
ary[0]=100;
ary=[100];
ary[0]=0;
console.log(ary);
}
fn(ary);
console.log(ary);
查找上级作用域
当前函数执行,形成一个私有作用域A,A的上级作用域是谁,和他在哪执行的没有关系,只和他在哪创建(定义)的有关系,在哪创建的,它的上级作用域就是谁
在传统ES5中,只有全局作用域和函数执行形成的私有作用域这两种作用域,循环或者判断等都不是作用域
arguments:实参集合 arguments.callee:函数本身FN arguments.callee.caller:当前函数在哪执行的,caller就是谁(记录的是它执行的宿主环境),在全局下执行的结果是null
var a=12;
function fn(){
console.log(arguments.callee.caller)//=>sum这个函数本身
}
function sum(){
var a=120;
fn();
}
练习
var n=10;
function fn(){
var n=20;
function f(){
n++;
console.log(n);
}
f();
return f;
}
var x=fn();
x();
x();
console.log(n);
JS中的内存分为堆内存和栈内存
堆内存
存储引用数据类型值(对象:键值对 函数:代码字符串)
堆内存释放
让所有引用堆内存空间地址的变量赋值为null即可(没有变量占用这个堆内存了浏览器会在空闲的时候把它释放掉)
栈内存
提供JS代码执行的环境和存储基本类型值
栈内存释放
一般情况下,当函数执行完成,所形成的私有作用域(栈内存)都会自动释放掉(在栈内存中存储的值也都会释放保证浏览器的高速运转),但是也有特殊不销毁的情况:
- 1.函数执行完成,当前形成的栈内存中,某些内容被栈内存以外的变量占用了,此时栈内存不能释放(一旦释放外面 找不到原有的内容了)
- 2.全局栈内存只有在页面关闭的时候才会被释放掉
如果当前栈内存没有被释放,那么之前在栈内存中存储的基本值也不会被释放能够一直保存下来
知识点补充
i++:自身累加1(先拿原有值计算,运算结束后,本身累加1)
++i:自身累加1(先自身累加1,拿累加后的结果进行运算)
var k=1;
console.log(5+(++k),k);//=>(7,2)
console.log(5+(k++),k);//=>(6,2)
-------------
var k=1;
console.log(5 + (++k) + (k++) + 4 + (k--) + (++k) + 3 + (--k) + (k++), k);//=>(26,3)
return后面返回的都是值,所以不管后面跟的是什么都不会进行变量提升
var i = 1;
function fn(i) {
return function (n) {
console.log(n + (++i));
}
}
var f = fn(2);
f(3);
fn(5)(6);
fn(7)(8);
f(4);
var i = 2;
function fn() {
i += 2;
return function (n) {
console.log(n + (--i));
}
}
var f=fn();
f(2);
f(3);
fn()(2);
fn()(3);
f(4);
let i=1;
let fn=function(n){
i*=2;
return function (m){
i+=n+m;
console.log(i);
}
};
let f=fn(2);
f(3);//=>7
fn(2)(3);//=>19
f(4);//=>25
f(5);//=>32
闭包
定义
函数执行形成一个私有的作用域,保护里面的私有变量不受外界的干扰,这种保护机制称之为"闭包" 部分开发者认为闭包是:形成一个不销毁的私有作用域(私有栈内存)才是闭包
//=>闭包:柯理化函数
function fn(){
return function (){
}
}
var f=fn();
------------------------------
//=>闭包:惰性函数
var utils=(function(){
return {
}
})();
闭包实战应用
真实项目中为了保证JS的性能(堆栈内存的性能优化),应该尽可能的减少闭包的使用(不销毁的堆栈内存是消耗性能的)
一、闭包的保护作用
保护私有变量不受外界的干扰
在真实项目中,尤其是团队协作开发的时候,应当尽可能的减少全局变量的使用,以防止相互之间的冲突("全局变量污染"),那么此时我们完全可以把自己这一部分内容封装到一个闭包中,让全局变量转化为私有变量
(function(){
var n=12;
function fn(){
}
})();
不仅如此我们封装类库插件的时候,也会把自己的程序都存放到闭包中保护起来,防止和用户的程序冲突,但是我们又需要暴露一些方法给客户使用,这样我们如何处理呢?
1.JQ这种方式,把需要暴露的方法抛到全局
(function(){
function jQuery(){
...
}
window.jQuery=window.$=jQuery//=>把需要供外面使用的方法,通过给window设置属性的方式暴露出去
})()
jQuery();
$();
2.Zepto这种方式:基于RETURN把需要供外面使用的方法暴露出去
var Zepto=(function(){
//...
return {
xxx:function(){
}
};
})();
Zepto.xxx()
//=>Zepto的值是自执行函数执行后返回的值,所以是return后面的对象,相当于Zepto={xxx:function(){}}
//Zepto.xxx,相当于等于xxx属性名对应的属性值
二、闭包的保存作用
形成不销毁的栈内存,把一些值保存下来,方便后面的调取使用
案例:选项卡
var oTab=document.getELementById('tab'),
tabList=oTab.getElementsByTagName('li'),
divList=oTab.getElementsByTagName('div');
function changeTab(curIndex){
for(var i=0;i<tabList.length;i++){
tabList[i].className="";
divList[i].className="";
}
tabList[curIndex].className="active";
divList[curIndex].className="active"
}
1、不可以直接使用索引i的原因
事件绑定是"异步编程",当触发点击行为,绑定的方法执行的时候,外层循环已经结束;方法执行产生私有作用域,用到变量i,不是私有的变量,按照"作用域链"的查找机制,找到的是全局下的i(此时全局的i已经成为循环最后一次的结果3)
for(var i=0;i<tabList.length;i++){
tabList[i].onclick=function{
changeTab(i);
}
}
1.
想要触发事件之前,首先页面需加载完毕,JS代码也已经加载完成,也就是该循环已经结束,此时的i变为了3
2.
当某个li被点击的时候会触发事件,执行函数,函数执行时会形成一个私有作用域,此时i不是私有变量,基于作用域链机制,会往上级作用域(也就是window作用域)查找,此时的i已经变为33.
所有的事件绑定都是异步编程
- 同步编程:一件事一件事的做,当前这件事没完成,下一个任务不能处理
- 异步编程:当前这个事件没有彻底完成,不再等待,继续执行下面的任务
- 绑定事件后,不需要等待执行,继续执行下一个循环任务,所以当我们点击执行方法的时候,循环早已结束(让全局的i等于循环最后的结果3)
2、使用闭包实现
利用闭包的机制,把后期需要的索引实现存储到自己的私有作用域中:"闭包有保存作用"
第一种
for(var i=0;i<tabList.length;i++){
tabList[i].onclick=(function(n){
//=>让自执行函数执行,把执行的返回值return赋值给onlick事件(此处onclick绑定的是返回的小函数,点击的时候执行的是小函数),自执行函数在给事件赋值的时候就已经执行了
var i=n;
return function(){
changeTab(i);//=>上级作用域:自执行函数形成的作用域
}
})(i);
}
循环三次,形成三个不销毁的私有作用域(自执行函数执行),而每一个不销毁的栈内存中存储了一个私有变量i,这个值分别是每一次执行传递进来的全局i的值(也就是:第一个不销毁的作用域存储的是0,第二个是1,第三个是2);当点击的时候,执行返回的小函数,遇到变量i,向它自己的上级作用域查找,找到的i值分别是:0/1/2,达到了我们想要的效果
var btnBox = document.getElementById('btnBox'),
inputs = btnBox.getElementsByTagName('input');
for (var i = 0; i < inputs.length; i++) {
inputs[i].onclick = (function (i) {
return function () {
alert(i);
}
})(i);//=>把每一次循环时候i(全局的)的值传递给自执行函数
}
第二种
for(var i+0;i<tabList.length;i++){
(function(n){
tabList[n].onclick=function(){
changeTab(n);
}
})(i)
}
//=>原理都是形成三个不销毁的私有作用域,分别存储需要的索引值
3、使用ES6方法实现
ES6知识点补充
基于ES6中的LET来创建的变量是存在块级作用域的(类似于私有作用域,有私有变量和作用域链查找机制),在大括号里面也是重新检测语法规范,看一下是否是基于新语法创建的变量,如果是,按照新语法规范来解析
ES6中的作用域
- 1.全局作用域
- 2.私有作用域(函数执行)
- 3.块级作用域(一般用大括号包起来的都是块级作用域,
对象除外
)
let a=100;
{
let a=200;
{
let a=300;
{
console.log(a);//=>300
}
}
}
//=>每个变量a是不一样的
-------------------------------
let a=100;
{
let a=200;
{
a=300;//=>把上级块级作用域中的a从200改为300;
{
console.log(a);//=>300,先往上级块级作用域找,没有变量a,再往上找有变量a了但是已经变成300了
}
}
}
---------------------------
let a=100;
{
let a=200;
{
{
console.log(a);//=>Uncaught ReferenceError: a is not defined
//=>当前块级作用域没有变量a,需要往上级块级作用域找,执行到当前代码的时候,还没有声明定义a,但是浏览器知道有a这个变量,所以会报错a is not defined
}
let a=300;
}
}
var a=12;
if(true){
console.log(a);//=>Uncaught ReferenceError: a is not defined
let a=13;
let b=12;
}
console.log(b)//=>Uncaught ReferenceError: b is not defined
//=>因为if里面是用let创建的所以是块级作用域,a和b相当于该块级作用域的私有变量,和外面是没有关系的
var a=12;
if(true){
console.log(a);//=>Uncaught ReferenceError: a is not defined
let a=13;
var b=12;
}
console.log(b)//=>12
if(1===1){
let fn=function (){
console.log(1);
};
console.log(fn);//=>fn是当前判断体块级作用域中私有的
}
console.log(fn);//=>Uncaught ReferenceError: fn is not defined
var a=12;
if(true){
console.log(a);//=>Uncaught ReferenceError: a is not defined
let a=13;
b=12;//=>相当于给全局作用域下加了一个window.b的属性
}
console.log(b)//=>12
console.log("b" in window)//=>true
解决选项卡
ES6和闭包的机制类似,ES6中使用LET创建变量,会形成块级作用域
for(let i=0;i<5;i++){
//=>循环体也是块级作用域,初始值设置的变量是当前块级作用域中的变量(形成了五个块级作用域,每个块级作用域中都有一个私有变量i,变量值就是每一次循环i的值)
}
console.log(i)//=>Uncaught ReferenceError: i is not defined
for(let i=0;i<tabList.length;i++){
tabList[i].onclick=function(){
changeTab(i);
}
}
第一次循环
{
let i=0;
{
tabList[i].onclick=function(){//=>i往上级块级作用域找,i=0
changeTab(i);
}
}
}
第二次循环
{
let i=1;
{
tabList[i].onclick=function(){i往上级块级作用域找,i=1
changeTab(i)
}
}
}
第三次循环
{
let i=2;
{
tabList[i].onclick=function(){i往上级块级作用域找,i=2
changeTab(i)
}
}
}
第四次循环
{
let i=3;
{
tabList[i].onclick=function(){i往上级块级作用域找,i=3
changeTab(i)
}
}
}
第五次循环
{
let i=4;
{
tabList[i].onclick=function(){i往上级块级作用域找,i=4
changeTab(i)
}
}
}
//=>每一次循环都形成一个块级作用域,而且块级作用域内都存储了当前循环的i的值
单例
单例设计模式(singleton )
表现形式
var obj={
xxx:xxx,
....
};
在单例设计模型中,OBJ不仅仅是对象名,它被称为"命名空间[NameSpace]",把描述事物的属性存放到命名空间中,多个命名空间是独立分开的,互不冲突
作用
:
把描述同一件事物的属性和特征进行"分组、归类"(存储在同一个堆内存空间中),因此避免了全局变量之间的冲突和污染
var parttern1={name:'xxx'};
var parttern2={name:'xxx'};
命名由来
每个命名空间都是JS中Object这个内置基类的实例,而实例之间是相互独立互不干扰的,所以我们称它为"单例:单独的实例"
高级单例模式
在给命名空间赋值的时候,不是直接赋值给一个对象,而是先执行自执行函数,形成一个私有作用域AA(不销毁的栈内存),在AA中创建一个堆内存,把堆内存地址赋值给命名空间(
惰性思想
)
这种模式的好处:我们完全可以在AA中创造很多内容(变量or函数),哪些需要供外面调取使用的,我们暴露到返回的对象当中(模块化实现的一种思想)
var nameSpace=(function(){
var n=12;
function fn(){
}
function sum(){
}
return {
fn:fn,
sum:sum
}
})();
nameSpace.fn();//=>相当于在外面调取fn这个函数并执行,nameSpace.fn是fn这个属性名对应的属性值,也就是fn
//=>nameSpace的值为自执行函数执行返回的值,也就是return后面的对象
this
当前方法执行的主体(谁执行的这个方法,那么this就是谁,所以this和当前方法在哪创建的或者在哪执行的都没有必然联系)
非严格模式下
第一种情况
给当前元素的某个事件绑定方法,当事件触发方法执行的时候,方法中的this是当前操作的元素对象
oBox.onclick=function(){
//=>this:oBox
}
第二种情况
普通函数执行,函数中的this取决于执行的主体。谁执行的,this就是谁(执行主体:方法执行,看方法名前面是否有
点
,有的话,点
前面是谁this就是谁,没有的话,this就是window)
function fn(){//=>AAAFFF000
console.log(1);
}
var obj={
fn:fn//=>fn:AAAFFF000
}
//=>执行的是相同的方法(不同的地方在于函数执行方法中的this是不一样的)
obj.fn();//=>this:obj
fn();//=>this:window
案例一
var name="window";
var Tom={
name:"Tom",
show:function(){
console.log(this.name);
},
wait:function(){
var fun=this.show;//=>此时的this是Tom
fun();//=>前面没有点,相当于直接执行该函数,此函数的this是window
}
};
Tom.wait();//=>window
Tom.show();//=>Tom
var length=10;
fuction fn(){
console.log(this);
console.log(this.length);
}
var obj={
length:5;
method:function(fn){
fn();
arguments[0]();
}
};
console.log(obj.method(fn,1));
window 10
arguments本身 2
第三种情况
自执行函数,一般情况下this是window
~function(){
//=>this:window
}();
练习题
var n = 2;
var obj={
n:3,
fn:(function (n) {
n*=2;
this.n+=2;
var n=5;
return function (m) {
this.n*=2;
console.log(m + (++n));
}
})(obj.n)//=>Uncaught TypeError: Cannot read property 'n' of undefined
};
var fn = obj.fn;
fn(3);
obj.fn(3);
console.log(n, obj.n);
报错的原因:
obj处是先开辟一个堆内存存储键值对,fn对应的属性值是自执行函数执行后返回的结果,而此时键值对存储还没有完成,也就是还没有和变量obj关联在一起,obj相当于undefined(obj在全局作用域下只声明未定义),undefined是基本数据类型没有属性,所以会报错
var n = 2;
var obj={
n:3,
fn:(function (n) {
n*=2;
this.n+=2;
var n=5;
return function (m) {
this.n*=2;
console.log(m + (++n));
}
})(n)
};
var fn = obj.fn;
fn(3);//=>this:window 9
obj.fn(3);//=>this:obj 10
console.log(n, obj.n);//=>(8,6)
var num=1,
obj={
num:2,
fn:(function(num){
this.num*=2;
num+=2;
return function (){
this.num*=3;
num++;
console.log(num);
}
})(num)
};
var fn =obj.fn;
fn();//=>4
obj.fn();//=>5
console.log(num,obj.num);//=>(6,6)
第四种情况
构造函数执行,方法体中的this是当前类的一个实例
function Fn(){
this.name="zhufeng";
this.age=18;
}
var f=new Fn();
console.log(f);//{name:"zhufeng",age:18}
第五种情况
箭头函数中没有自己的this,this是上下文中的this
let obj={
fn:function(){
//=>this:obj
setTimeout(()=>{
//=>this:obj,继承了fn里面的this
},1000);
}
}
第六种情况
小括号表达式中,会影响this的指向
let obj={
fn:function(){
console.log(this);
}
};
obj.fn();//=>this:obj
(12,obj.fn)();//=>this:window
第七种情况
使用call/apply/bind可以改变this指向
fn.call(obj);//=>this:obj
fn.call(12);//=>this:12
fn.call();//=>this:window
非严格模式下call/apply/bind第一个参数不写或者写null和undefined,this都是window,严格模式下写谁this就是谁,不写是undefined
严格模式
开启严格模式:在当前作用域的第一行加上
'use strict'
(快捷键 us+[TAB]),那么当前作用域下再执行的JS代码都是按照严格模式处理的
'use strict';
//=>当前JS代码都开启了严格模式(包含了函数中的代码)
~function(){
'use strict';
//=>只是把当前私有作用域开启了严格模式(对外面全局没有影响)
}();
第一种情况
在JS严格模式下,如果执行主体不明确,this指向的是undefined(非严格模式下指向的是window)
function fn(){
console.log(this);
}
fn();//=>window
window.fn();//=>window
"use strict";
function fn(){
console.log(this);
}
fn();/=>undefined
window.fn();//=>window
第二种情况
自执行函数中的this为undefined
function fn(){
console.log(this);//=>window
}
document.body.onclick=function (){
console.log(this)//=>document.body
fn();
}
单例设计模块化开发
1.团队协作开发的时候,会把产品按照功能板块进行划分,每一个功能板块有专人负责开发
//=>搜索板块
var searchModel={
submit:function(){
untils.aa();
//=>可以直接调取公共板块的方法
}
}
//=>频道板块
var channelModel={
setChannel:function(){}
translateChannel:function(){}
show:function(){
secrchModel.submit();
//=>在当前的命名空间下调取其它命名空间的方法:制定好对应的空间名字即可,使用[nameSpace].[property]就可以操作
this.setChannel();
//=>调取本模块中的一些方法,可以直接使用this处理即可:此方法中的this一般都是当前模块的命名空间
}
}
channelModel.show();//=>代表调取channelModel下的show方法
//=>天气板块
var weatherRender=(function(){
var fn=function(){};
var f=function(){};
//=>想暴露在外面的方法就写在return里面
return {
init:function(){
fn();//=>调取自己板块中的方法直接调取使用即可
channelModel.show();
},
fn:fn,
aa:channnelModel.show
}
})();
weatherRender.init();
2.把各个板块之间公用的部分进行提取封装,后期再想实现这些功能,直接的调取引用即可
//=>公共板块
var utils=(function(){
return {
aa:function(){
}
}
})();
//=>搜索模块
var searchModel={
submit:function(){
untils.aa();
//=>可以直接调取公共板块的方法
}
}
``
工厂模式(Factory Pattern)
1.把实现相同功能的代码进行"封装",以此来实现"批量生产"(后期想要实现这个功能,我们只需要执行函数即可) 2."低耦合高内聚":减少页面中的冗余代码,提高代码的重复使用率
function createPerson(name,age){
var obj={};
obj.name=name;
obj.age=age;
return obj;
}
var p1=createPerson('xxx',25);
var p2=createPerson('xxx',25);
面向对象(OOP)
JS是一门编程语言(具备编程思想)
- [面向对象]
- JS\JAVA\PHP\C#\Ruby\Python\C++
- [面向过程]
- C
对象、类、实例
对象
:万物皆对象(我们所要研究学习以及使用的都是对象)
类
:对象的具体细分(按照功能特点进行分类:大类、小类)
实例
:类中具体的一个事物(拿出类别中的具体一个实例进行研究,那么当前类别下的其它实例也具备这些特点和特征)
整个JS就是基于面向对象设计和开发出来的语言,我们学习和实战的时候也要按照面向对象的思想去体会和理解
JS中的内置类
- Object:对象类(基类),每一个对象都是它的一个实例
- Number:每一个数字或者NaN是它的一个实例
- String:字符串类
- Boolean:布尔类
- Null:浏览器屏蔽了操作Null这个类
- Undefined:浏览器屏蔽了操作Undefined这个类
- Array:数组类
- RegExp:正则类
- Date:日期类
- Function:函数类
- HTMLCollection:元素集合类
- NodeList:节点集合类
- EventTarget
为什么getElmentById只能通过document获取?
因为getElementById这个方法只有Document这个类才有,其它类没有,document是Document这个类的一个实例,Document这个类同时也提供getElementsByTagName/getElementsByClassName等方法。
页面中的其他元素都是Element类的一个实例,Element只提供getElementsByTagName/getElementsByClassName等方法,没有getElementById这个方法。
constructor(构造函数模式)
创建某个类的一个实例的方法
var a=new 类名();
var num=new Number(1);
//=>num是Number类的一个实例,值为1
---------------------------------
var ary=new Array();
//=>ary是Array类的一个实例
1.new Array(10):创建一个长度为10的数组,数组中的每一项都是空
2.new Array("10"):如果只传递一个实参,并且实参不是数字,相当于把当前值作为数组的第一项存储进来
3.new Array(10,20,30):如果传递多个实参,不是设置长度,而是把传递的内容当做数组中的每一项存储起来
------------------
var obj=new Object();
//=>一般只用于创建空对象,如果需要增加键值对,创建完成后自己依次添加("对象.属性名"或者"对象[属性名]"的方式)
基于构造函数创建自定义类
1、在普通函数执行的基础上"new xxx()",这样就不是普通函数执行了,而是构造函数执行,当前的函数名称之为"类名",接收的返回结果是当前类的一个实例 2、自己创建的类名,最好第一个单词首字母大写 3、这种构造函数设计模式执行,主要用于组件、类库、插件、框架等的封装,平时编写业务逻辑一般不这样处理
JS中引用数据类型创建值的两种方式
不管是哪一种方式创造出来的都是Object类的实例,而实例之间是独立分开的,所以 var xxx={},这种模式就是JS中的单例模式
1、
字面量表达式
var obj ={};//=>这种模式是JS的单例模式
2、
构造函数模式
var obj = new Object();
JS中基本数据类型创建值的两种方式
1、基于字面量方式创建出来的值是基本类型值 2、基于构造函数创建出来的值是引用类型(对应的实例都是对象类型的)
var num1=12;
var num2=new Number(12);
console.log(typeof num1);//=>"number"
console.log(typeof num2);//=>"object"
//=>num1是数字类的实例,num2也是数字类的实例,num2只是JS表达数字的方式之一,都可以使用数字类提供的属性和方法
构造函数执行的步骤
1、形成私有作用域(栈内存)
- 形参赋值
- 变量提升
2、
【构造函数执行独有】
在JS代码自上而下执行之前,首先在当前形成的私有栈内存中创建一个对象(创建一个堆内存:暂时不存储任何的东西),并且让函数中的执行主体(THIS)
指向这个新的堆内存
3、代码自上而下执行 ,
this.xxx=xxx这类操作都是在给创建的这个对象(this)增加属性名和属性值
4、
【构造函数独有】
代码执行完成,把之前创建的堆内存地址返回(浏览器默认返回),那么开始创建的对象其实就是当前这个类的一个实例。
function Fn(){
var n=10;
this.name=name;
this.age=age+n;
}
var f=new Fn();//=>在构造函数执行的时候,如果Fn不需要传递实参,我们可以省略小括号,意思还是创建实例(和加小括号没有区别)
如果构造函数中,写了RETURN
1.return后面的代码都不执行
2.return后面什么也不写,返回的还是this指向的那个对象了
3.return后面是一个基本数据类型值,返回的结果依然是this指向的对象
4.return后面是一个引用数据类型值,则会把默认返回的实例覆盖,此时接收到的结果就不在是当前类的实例了
function Fn(){
var n=10;
this.m=n;
return [12,23];
this.a=20;
}
var f=new Fn();
console.log(f)//=>[12,23]
------------------
function Fn(){
var n=10;
this.m=n;
return "珠峰";
this.a=20;
}
var f=new Fn();
console.log(f)//=> f={m:10}
检测方法补充
instanceof
检测某一个实例是否隶属于这个类(不能检测基本数据类型只能检测对象类型)
in
检测当前对象是否存在某个属性(不管当前这个属性是对象的私有属性还是公有属性,只要有结果就是TRUE)
in可以检测到自己添加的属性和浏览器内置的私有属性以及原型上的内置的属性及手动添加的属性
let obj={name:"zhufeng",age:18};
"toString" in obj;//=>true
一定要注意添加双引号,属性名都是字符串或者数字,不加,浏览器会当做变量来处理
hasOwnProperty
检测当前属性是否为对象的私有属性(不仅要有这个属性,而且必须还是私有的才可以)
浏览器内置的那些属性是不可枚举的,自己手动添加的是可枚举的
hasOwnProperty只能找到可枚举的私有属性
特殊情况:JS的类状图,除了第一排的那些类以及window,Winow(例如:Array,Number,Function,EvenTarget),使用hasOwnProperty·可以检测到浏览器内置的私有属性,其他是不行的
let box=document.getElementById("tabBox");
box.aa=200;
box.hasOwnProperty("className");//=>flase
box.hasOwnProperty("aa");//=>true
//className是box元素对象的内置属性
//aa是我们自己手动给box元素对象添加的内置属性
-------------------
let obj={name:"zhufeng",age:9};
obj.hasOwnProperty("name");//=>true
obj.hasOwnProperty("toString");//=>false
//=>toString是原型上的公有属性
练习:封装hasPubProperty方法
function hasPubProperty(obj,attr){
while(attr in obj){
if(obj.hasOwnProperty(attr)){
return false;
}
return true;
}
return false;
}
原型和原型链
函数和对象的分类
[函数] 普通函数、
类
(所有的类:内置类、自己创建的类、Date)
[对象] 普通对象、数组、正则、Math、arguments...
实例是对象类型的
(除了基本数据类型的字面量创建的值)prototype的值也是对象类型的
函数也是对象类型的
原型
1、所有的函数数据类型都天生自带一个属性:prototype(原型),这个属性的值是一个
对象
,这个对象中存储了当前类供其实例调取使用的公有属性和方法,浏览器会默认给它(prototype)开辟一个堆内存
2、在浏览器给prototype开辟的堆内存中有一个天生自带的属性:
constructor
,这个属性存储的值是当前函数本身3、每一个对象都有一个
_proto_
(原型链)的属性,这个属性指向当前实例所属类的prtototype(如果不能确定它是谁的实例,都是Object的实例)所有函数数据类型都天生自带name和length两个属性,name存储的是该函数的函数名(字符串),length存储的是形参的数量
function fn(x,y,z){
console.log(fn.name);//=>"fn"
console.log(fn.length);//=>3
}
fn(10);
--------------
var f=function (x,y,z){
console.log(f.name);//=>"f"
console.log(fn.length);//=>3
};
f(10);
------------------
function fn(){
return function(x,y,z){
}
}
var a=fn();
console.dir(a);
//=>name:""
//=>length:3
function Fn(){
var n=100;
this.AA=function(){
console.log("AA[私]")
};
this.BB=function(){
console.log("BB[私]")
};
}
Fn.prototype.AA=function(){
console.log("AA[公]")
};
var f1=new Fn;
var f2=new Fn;
原型链
它是一种基于__proto__向上查找的机制。当我们操作实例的某个属性或者方法的时候,首先找自己空间中私有的属性或者方法
1.找到了,则结束查找,使用自己私有的即可
2.没有找到,则基于__proto__找所属类的prototype,如果找到就用这个公有的,如果没有找到,基于原型上的__proto__继续向上查找,一直找到Object.prototype的原型为止,如果还没有,则操作的属性或者方法不存在,返回undefined
function Fn() {
this.x = 100;
this.y = 200;
this.getX = function () {
console.log(this.x);
}
}
Fn.prototype.getX = function () {
console.log(this.x);
};
Fn.prototype.getY = function () {
console.log(this.y);
};
var f1 = new Fn;
var f2 = new Fn;
console.log(f1.getX === f2.getX);//=>false
console.log(f1.getY === f2.getY);//=>true
console.log(f1.__proto__.getY === Fn.prototype.getY);//=>true
console.log(f1.__proto__.getX === f2.getX);//=>false
console.log(f1.getX === Fn.prototype.getX);//=>false
console.log(f1.constructor);//=>Fn
console.log(Fn.prototype.__proto__.constructor);//=>Object
f1.getX();//=>this:f1 =>console.log(f1.x); =>100
f1.__proto__.getX();//=>this:f1.__proto__ =>console.log(f1.__proto__.x); =>undefined
f2.getY();//=>this:f2 =>console.log(f2.y); =>200
Fn.prototype.getY();//=>this:Fn.prototype =>console.log(Fn.prototype.y); =>undefined
私有属性和共有属性
私有属性
自己堆内存中存储的属性相对自己来说是私有的
公有属性
自己基于__proto__找到的属性,相对自己来说是共有的
function fun(){
this.a=0;
this.b=function(){
alert(this.a);
}
}
fun.prototype={
b:function (){
this.a=20;
alert(this.a);
},
c:function (){
this.a=30;
alert(this.a)
}
};
var my_fun=new fun();
my_fun.b();
my_fun.c();
构造原型设计模式
在实际项目基于面向对象开发的时候(构造原型设计模式),我们根据需要,很多时候会重定向类的原型(让类的原型指向自己开辟的堆内存)
存在的问题
1、
自己开辟的堆内存中没有constructor属性,导致类的原型构造函数缺失(解决:自己手动在堆内存中增加constructor属性)
2、
当原型重定向后,浏览器默认开辟的那个原型堆内存会被释放掉(在没有被占用的情况下),如果之前已经存储了一些方法或者属性,这些东西都会丢失,所以内置类的原型不允许重定向到自己开辟的堆内存
,因为内置类原型上自带很多属性方法,重定向后都没了,这样是不被允许的,浏览器默认不让修改,就算修改了也没用。
function Fn(){
console.log("ok");
}
var f1=new Fn();
Fn.prototype={
name:"zhufeng",
age:18
}
var f2=new Fn();
当我们需要给自定义类的原型批量设置属性和方法的时候,一般都是让原型重定向到自己创建的对象中
function Fn={
}
Fn.prototype={
constructor:Fn,
aa:function(){
}
}
练习题
function Fn() {
var n = 10;
this.m = 20;
this.aa = function () {console.log(this.m);}
}
Fn.prototype.bb = function () {console.log(this.n);};
var f1=new Fn;
Fn.prototype={
aa:function(){console.log(this.m+10);}
};
var f2=new Fn;
console.log(f1.constructor);//=>f Fn
console.log(f2.constructor);//=>f Object
f1.bb();//=>undefined
f1.aa();//=>20
f2.bb();//=>报错
f2.aa();//=>20
f2.__proto__.aa();//=>NaN
####内置类的原型扩展方法
设置内置方法,可以供它的实例调取使用
1.
手动增加的方法最好设置"my"前缀(自己定),防止把内置方法重写
//=>方法中的this一般都是当前类的实例(也就是我们要操作的数组)
//=>操作this相当于操作ary,方法执行完成会改变原有数组
Array.prototype.myUnique=function myUnique(){
var obj={};
for(var i=0;i<this.length;i++){
var item=this[i];
if(item in obj){
this[i]=this[this.length-1];
this.length--;
i--;
continues;
}
obj[item]=item;
}
obj=null;
};
ary.myUnique();//=>this:ary
练习:给Number类增加自定义方法plus和minus
plus:加一个数 minus:减一个数
Number.prototype.plus=function plus(n){
this=this+Number(n)//=>1、this是不能被赋值 2、此时左边this相当于变量,变量也不能使用关键字
return this+Number(n);
}
Number.prototype.minus=function minus(m){
return this-Number(m)
}
JS中的链式写法
保证每一个方法执行返回的结果依然是当前类的实例,这样就可以继续调取方法使用了
ary.sort(function (a,b){return a-b};).reverse().pop();
//=>pop方法的返回值是删除的那一项
---------------------------------
ary.sort(function (a,b){return a-b;}).reverse().slice(2,7).join("+").split("+").toString().substr(2).toUpperCase();
函数的三种角色
1、
普通函数
- 堆栈内存释放
- 作用域链
2、
类
- prototype:原型
- _proto_:原型链
- 实例
3、
普通对象
function Fn(){
var n=10;
this.m=100;
}
Fn.prototype.aa=function (){
console.log("aa");
}
Fn.bb=function (){
console.log("bb");
}
var f=new Fn();
JQ这个类库中提供了很多方法,其中有一部分写在原型上,有一部分是把它当做普通对象来设置的
~function(){
function jQuery(){
return [JQ实例]
}
jQuery.prototype.animate=function(){};
jQuery.ajax=function(){};
window.jQuery=window.$=jQuery;
}();
$()//=>是JQ的一个实例,只能调取jQuery原型上的属性
$().ajax()//=>调取不了,那是jQuery类本身的属性
$().animate()//=>可以调取
$.ajax()//=>直接的对象键值对操作
$.animate()//=>对象上没有animate这个属性,这个属性和实例相关的原型上
函数三种角色的运行机制图
function Fn() {
this.n = 100;
}
Fn.prototype.getN = function () {
console.log(this.n);
};
Fn.AA = 200;
var f=new Fn();
阿里面试题
function Foo() {
getName = function () {
console.log(1);
};
return this;
}
Foo.getName = function BBB() {
console.log(2);
};
Foo.prototype.getName = function AAA() {
console.log(3);
};
var getName = function () {
console.log(4);
};
function getName() {
console.log(5);
}
Foo.getName();//=>2 把Foo当做一个对象,找Foo的私有方法执行
getName();//=>4 执行全局下的GET-NAME
Foo().getName();//=>1 先把FOO当做普通函数执行,执行返回的结果(this为window)在调取GET-NAME执行
getName();//=>1 执行的依然是全局下的GET-NAME
new Foo.getName();//=>2 成员访问的优先级高于无参数列表new =>Foo.getName([F->2])=>new [F->2]()
new Foo().getName();//=>3 成员访问和有参数列表new优先级都是19,按照从左到右依次执行即可 =>创造Foo的一个实例f =>f.getName()=>3
new new Foo().getName();//=>3 先算new Foo(),创造一个实例f =>new f.getName() =>继续执行优先级高的成员访问 =>f.getName =>[F->3] =>new [F->3]()=>构造函数执行这个方法
Function类的call/apply/bind方法
call/apply/bind用来改变某一个函数中的this关键字指向的 只要是函数都可以调取这三个方法 ####call方法
语法
:[fn].call([this],[param].....) fn.call():当前实例(函数Fn)通过原型链的查找机制,找到Function.prototype上的call方法,把call方法执行
call方法执行的步骤
1、
把call方法中的this的"this关键字"改为arguments[0]2、
把arguments中除了第一项以外的实参传递给this,执行this
fn.call();
Function.prototype.call=function (){
let param1=arguments[0],
paramOther=[];//=>把arguments中除了第一个以外的实参获取到
//=>call方法中的this:fn 当前要操作的函数(函数类的一个实例)
//把Fn中的关键字修改为param1=>把this(call中)的this关键字修改为param1
this.toString().replace("this",param1);
this(paramOther);
//=>把fn执行,把paramOther分别传递给fn
}
细节
1.非严格模式下,如果参数不传,或者第一个传递的是null/undefined,this都指向window 2.在严格模式下,第一个参数是谁,this就指向谁(包括null/undefined),不传this是undefined
let fn=function (a,b){}
fn.call(obj,10,20);//=>this:obj a=10 b=20
fn.call(10,20);//=>this:10 a=20 b=undefined
fn.call();//=>this:window a=undefined b=undefined
fn.call(null);//=>this:window
fn.call(undefined);//=>this:window
function fn1(){console.log(1);console.log(this);}
function fn2(){console.log(2);console.log(this);}
fn1.call(fn2);
fn1.call.call(fn2);
//fn1.call相当于Function类原型上的call方法
//call方法也是一个函数,所以也可以调取Function类原型上的call方法,那么相当于call方法(先命名为A)调取了call方法并执行
//call方法执行的第一步先把this中的“this”关键字改为fn2,也就是把A(call方法中的this改为fn2)
//call方法执行的第二步把this执行,也就是A(call方法)执行
//A执行也就是call方法又执行一遍,无实参,此时call方法中的this已经变为fn2了,那么第一步把this中的“this关键字”改为实参第一项(undefined),第二步把this执行也就是把fn2执行,无传参,输出2
非严格模式下执行,控制台会输出this是window,因为没有传参数,所以this指向window,切换到严格模式下控制台就会输出undefined了
Function.prototype.call(fn1)
//=>线找到call把它执行,call中的this是Function.prototype=>让F.P中的this变为FN1,然后让F.P执行,F.P是一个匿名函数也是一个空函数,执行没有返回值,也就是没有任何输出
Function.prototype.call.call(fn1);
//=>先找到CALL-AA把它执行,它中的THIS是F.P.CALL =>把F.P.CALL中的THIS修改为FN1,让F.P.CALL执行 =>F.P.CALL(CALL-AA)第二次把它执行(此时它里面的THIS已经是FN1) =>这一次其实在CALL-AA中是让FN1执行 =>1
规律
1.一个call,执行的是call前面的函数 2.两个及两个以上call,执行的是括号中传的第一个实参
apply方法
语法和call基本一模一样,唯一的区别在于传参方式,apply是把这些值放到一个数组或者类数组中,但是也相当于一个个的传递给函数(语法不一样但是作用一模一样)
fn.call(obj,10,20)
fn.apply(obj,[10,20])
//=>apply把需要传递给fn的参数放到一个数组(或者类数组)中传递进去,虽然写的是一个数组,但是也相当于给fn一个个的传递
案例:获取数组中的最大值
第一种
给数组先排序(由大到小排序),第一项就是最大值
let ary=[12,23,4,5,2,67,89];
let max=ary.sort(function (a,b){return b-a;})[0];
console.log(max);
第二种
假设法:假设第一个值时最大值,依次遍历数组中后面的每一项,和假设的值进行比较,如果比假设的值要大,把当前项赋值给MAX
let ary=[12,3,4,2,32,34,12,45,23,4,5,34];
let max=ary[0];
for(let i=1;i<ary.length;i++){
let item=ary[i];
item>max?max=item:null;
}
console.log(max);
第三种
基于Math.max和eval方法实现
let ary=[12,3,4,2,32,34,12,45,23,4,5,34];
ary=ary.toString()
let max=eval("Math.max("+ary+")");
console.log(max);
//=>Math.max("12,23,1,24"),此时会把"12,23,1,24"当做一项,然后调用Number()方法,转换为数字,所以会输出NaN
//=>Math.max(12,23,45,"89")=>89
知识点扩充:括号表达式
用小括号包起来,里面有很多项(每一项用逗号分隔),最后只获取最后一项的内容(但是会把其他的项也都过一遍)
(function(){console.log(1);},function(){console.log(2);})();//=>2
-----------------------
let a=1===1?(12,23,14):null;//=>14
不建议大家过多使用括号表达式,因为会改变this
let fn=function(){console.log(this);}
let obj={fn:fn};
(fn,obj.fn)();//=>执行的是第二个obj.fn,但是方法中的this是window而不是obj
(obj.fn)();//=>this:obj
第四种
基于Math.max和apply方法实现
let ary=[12,3,4,2,32,34,12,45,23,4,5,34];
let max=Math.max.apply(null,ary);
console.log(max);
//=>Math.max是一个函数,所以可以调取Function.prototype上的apply方法
//=>apply方法:
//1.把this中的“this关键字”改为arguments[0]
//2.把this执行,并把arguments[1]...这些实参传递给this(以数组的形式传参),虽然是数组的形式传参,但是也是一个一个的传递
第五种
基于ES6中的展开运算符
let max=Math.max(...ary);
console.log(max);
bind方法
语法和call一模一样,唯一的区别在于立即执行还是等待执行
fn.call(obj,10,20)
//=>改变fn中的this,并且把fn立即执行
fn.bind(obj,10,20)
//=>改变fn中的this,此时的fn并没有执行
案例
需求:点击的时候执行fn,让fn的this是obj
let fn=function (a,b){
console.log(this);
};
let obj={name:"OBJ"};
document.onclick=fn;//=>this:document
document.onclick=fn.call(obj);//=>虽然this确实改为obj了,但事后绑定的时候就把fn执行了(call是立即执行函数),点击的时候执行的是fn的返回值undefined
document.onclick=fn.bind(obj);//=>bind属于把fn中的this预处理为obj,此时fn没有执行,当点击的时候才会把fn执行
案例:定时器
//=>设置一个定时器,把一个方法赋值给定时器,设定等待的时间,当到达时间后,把方法执行(1000MS)
setTimeout(function(){console.log(1);},1000);
------------------
setTimeout(fn.call(obj,10,20),1000);
//=>call是立即执行,在设置定时器的时候,就把fn执行了(此时也基于call改变了this),把fn执行的返回值赋值给定时器,1000ms后执行的是fn的返回值
--------------------
setTimeout(fn.bind(obj,10,20),1000);
解构赋值
按照一个数据值的结构,快速解析获取到其中的内容,真实项目中一般都是针对于数组或者对象进行解构赋值
数组
让等号左边出现和右边相同的数据结构,左边可以创建一些变量快速获取到右侧对应位置的值
let ary=[1,2,3];
let [a,b,c]=ary;
console.log(a,b,c);
//=>a和b互换位置
let a=12,
b=13;
[a,b]=[b,a];
console.log(a,b);
获得数组中的某一项
let ary=[1,2,3,4,5];
let [,b,,c,d=0]=ary;
console.log(b,c,d)//=>2,4,0
//=>d默认赋值为0,如果当前项没有解构到任何值,给一个初始值;如果d不赋值默认值的话得到的应该是undefined
-------------
let ary=[1,2,3,4,5];
let [a]=ary;
console.log(a);//=>1
拓展运算符
"..."称之为拓展运算符:除了前面以外的项,都放在一个数组中 剩余运算符只能处于结构中最后的位置
//=>需求:获取第一项,把剩下的项作为一个数组返回
let ary=[12,23,44,54,34];
let [a,...b]=ary;
console.log(a,b);//=>12 [23,44,54,34]
--------------------
let ary=[12,23,44,54,34];
let [a,...b,c]=ary//=>Uncaught SyntaxError: Rest element must be last element
function fn(context,...arg){
console.log(context,arg);
//=>arg是个数组,arguments是类数组,这样arg就可以用数组的方法
}
fn(obj,10,20,30);
展开运算符
把数组(对象/类数组)中的每一项展开
let ary=[1,23,12,45]
let max=Math.max(...ary);
console.log(max)//=>45
----------------
let ary=[1,2,3,4,5];
let [...newAry]=ary;//=>数组克隆
let newAry=ary.slice();
-----------------
let ary=[1,2,3,4,5];
let b=[...ary,1,2,3];
console.log(b);//=>[1,2,3,4,5,1,2,3]
对象
对象解构赋值默认情况下要求:左边
变量名
和对象中的属性名
一致才可以
let obj={name:"xxx",age:12,sex:0};
let {name,age}=obj;
console.log(name,age);//=>"xxx",12
let {sex}=obj;
console.log(sex);//=>0
let data={
name:"xxx",
age:27,
score:[110,130,20]
};
let {name,score:[chinese,,english=0]}=data;
console.log(name,chinese,english);//=>("xxx",110,20)
起别名
给解构的属性名起别名作为我们使用的
变量
let obj={name:"xxx",age:12,sex:0};
let {age:ageAA}=obj;
console.log(ageAA);//=>12
console.log(age);//=>Uncaught ReferenceError: age is not defined
给不存在的属性设置默认值
let obj={name:"xxx",age:12,sex:0};
let {friend=0}=obj;
console.log(friend);//=>0
--------------
let obj={name:"xxx",age:12,sex:0};
let {hobby:myHobby="lanqiu"}=obj;
let {name:myName="aa"}=obj;
console.log(myName, myHobby);//"xxx",12
剩余运算符
剩余运算符只能处于结构中最后的位置
let obj={name:"zhuefn",age:19,sex:0,hobby:"bas"};
let {name:a,age:b,...c}=obj;
console.log(a, b, c);
//=>"zhuefn",19,{sex:0,hobby:"bas"}
----------------
let obj={name:"zhuefn",age:19,sex:0,hobby:"bas"};
let {name:a,age:b,...c,d}=obj;
console.log(a, b, c,d);//=>Uncaught SyntaxError: Rest element must be last element
//=>把传递的对象解构了(不传递值,默认赋值为空对象),解构的时候如果某个属性不存在,我们赋值默认值
let fn=function (
{
name:myName="zhufeng",
age:myAge=0
}={}
){
console.log(myName, myAge);
console.log(name,age);//=>name相当于window.name(name是window这个实例自带的属性,属性值为空)
//=>报错:age is not defined
};
fn({name:"xxx",age:18});
展开运算符
let obj={name:"xxx",age:20};
let newObj={...obj,sex:0};//=>{name:"xxx",age:20,sex:0} 把原有的对象克隆一份
案例:任意数求平均数
需求:编写一个方法fn,实现任意数求平均数(去除数字中的最大和最小,然后在算平均数,保留小数点后面两位)
let fn=function(){
let ary=[];
for(let i=0;i<arguments.length;i++){
let item=arguments[i];
ary.push(item);
}
ary.sort(function(a,b){return a-b});
ary.pop();
ary.shift();
let average=eval(ary.join("+")/ary.length);
return average.toFixed(2);
}
重写slice方法
mySlice方法,克隆数组
Array.prototype.mySlice=function mySlice(){
let curary=[];
for(let i=0;i<this.length;i++){
let item=this[i];
curary.push(item);
}
return curary
}
arguments借用数组方法
前提类数组和数组类似,都有length和索引
let fu=function (){
let ary=[].slice.call(arguments,0);
ary.sort(function(a,b){return a-b});
ary.pop();
ary.shift();
let average=eval(ary.join("+")/ary.length);
return average.toFixed(2);
}
let fn=function (){
let arg=arguments;
[].sort.call(arg,function(a,b){return a-b;});
[].pop.call(arg);
[].shift.call(arg);
return eval([].join.call(arg,"+"))/arg.length;
}
let fn=function (){
let arg=arguments;
arg.__proto__=Array.prototype;
//=>在IE浏览器中屏蔽开发者使用__proto__
arg.sort(function (a,b){return a-b});
arg.pop();
arg.shift();
return eval(arg.join("+"))/arg.length;
}
使用展开运算符
let fn=function (...ary){
ary.sort(function (a, b) {
return a - b;
}).pop();
ary.shift();
return (eval(ary.join('+')) / ary.length).toFixed(2);
}
let fn=function(){
let ary=[...arguments];
ary.sort(function (a, b) {
return a - b;
}).pop();
ary.shift();
return (eval(ary.join('+')) / ary.length).toFixed(2);
}