1 声明
1-1 [js函数声明的三种方式:]
(1)Function()构造器
var f = new Function()
(2)函数声明
function f (){
console.log(111);
}
(3)函数表达式
var f = function(){
console.log(222);
}
1-2 [js变量声明:]
1.var声明的变量会挂载在window上,而let和const声明的变量不会
2.var声明变量存在变量提升,let和const不存在变量提升(严格来说,let也存在)
3.let和const声明形成块作用域
4.let存在暂存死区
5.const声明必须赋值
(1)var声明的变量会挂载在window上,而let和const声明的变量不会:
var a = 1;
console.log(a,window.a); // 1 1
const b = 10;
console.log(b,window.b); // 10 undefined
let c = 100;
console.log(c,window.c); // 100 undefined
(2)var声明变量存在变量提升,let和const不存在变量提升
console.log(a); // undefined ==> a已经声明还没有赋值,默认得到undefined值
var a = 1;
console.log(b); // 报错:b is not defined ==> 找不到b这个变量
const b = 10;
console.log(c); // 报错:c is not defined ==> 找不到c这个变量
let c = 100;
(3)let和const声明形成块作用域
f(1){
var a = 100;
let b = 10;
}
console.log(a); //100
console.log(b); //b is not defined
if(1){
var a = 100;
const b = 1;
}
console.log(a); //100
console.log(c); //b is not defined
(4)同一作用域下let和const不能重复声明,而var可以
var a = 100;
console.log(a); //100
var a = 10;
console.log(a); //10
let b = 100;
console.log(b);
let b = 10;
console.log(b); //Identifier 'b' has already been declared ==> 标识符已经被声明过了
(5)暂存死区
var a = 100;
if(1){
a = 10;
let a = 1;
//在当前块作用域中存在a使用let/const声明的情况下,给a赋值10时,只会在当前作用域查找变量a,
// 而这时,还未到声明时候,所以控制台Error:a is not defined
// 即let 和 const 不会声明提前
}
(6)const
一旦声明必须赋值,不能使用null占位。
声明后不能再修改
如果声明的是引用类型数据,可以修改其属性
const a = 100;
const list = [];
list[0] = 10;
console.log(list); // [10]
const obj = {a:100};
obj.name = 'apple';
obj.a = 10000;
console.log(obj); // {a:10000,name:'apple'}
1-3 js为什么要进行变量提升
造成变量声明提升的本质原因是:
js 引擎在代码执行前
有一个解析
的过程,创建了执行上下文
,初始化
了一些代码执行时
需要用到的对象
。
当访问
一个变量
时,会到当前执行上下文中的作用域链
中去查找,而作用域链的首端
指向的是当前执行上下文的变量对象
,这个变量对象是执行上下文的一个属性,它包含了函数的形参、所有的函数和变量声明,这个对象的是在代码解析的时候创建的。
首先要知道,JS在拿到一个变量或者一个函数的时候,会有两步操作,即解析
和执行
。
在解析阶段,JS会检查语法,并对函数进行预编译。解析的时候会先创建一个全局执行上下文环境,先把代码中即将执行的变量、函数声明都拿出来,变量先赋值为undefined,函数先声明好可使用。
在一个函数执行之前,也会创建一个函数执行上下文环境,跟全局执行上下文类似,不通过函数执行上下文会多出this,arguments和函数参数。
- 全局上下文:变量定义,函数声明
- 函数上下文:变量定义,函数声明,this,arguments
在执行阶段,就是按照代码的顺序依次执行。
那为什么会进行变量提升你呢?主要有以下两个原因:
- 提高性能
- 容错性更好
1.提高性能
在JS代码执行之前,会进行语法检查和预编译,并且这一操作只进行一次。这么做就是为了提高性能,如果没有这一步,那么每次执行代码前都必须重新解析一遍该变量(函数),而这是没有必要的,因为变量(函数)的代码并不会改变,解析一遍就够了。
2.容错性更好
变量提升可以在一定程度上提高JS的容错性,看下面的代码:
a = 1;var a;console.log(a);
如果没有变量提升,这两行代码就会报错,但是因为有了变量提升,这段代码就可以正常执行。 虽然,在可以开发过程中,可以完全避免这样写,但是有时代码很复杂的时候。可能因为疏忽而先使用后定义了,这样也不会影响正常使用。由于变量提升的存在,而会正常运行。
总结:
- 解析和预编译过程中的声明提升可以提高性能,让函数可以在执行时预先为变量分配栈空间
- 声明提升还可以提高JS代码的容错性,使一些不规范的代码也可以正常执行
2 数据类型的分类:
2-1 基本类型:
- string(字符串)--原始类型
- boolean(布尔值)--原始类型
- number(数字)--原始类型
- symbol(符号)--原始类型
- null(空值)
- undefined(未定义)
- BigInt(BigInt数据类型的目的是比Number数据类型支持的范围更大的整数值,精度在(2^53-1)范围内,BigInt(10) 值为:10n) ES2020新出的
其中 Symbol
和 BigInt
是ES6
中新增的数据类型:
- Symbol 代表创建后独一无二且不可变的数据类型,它主要是为了解决可能出现的全局变量冲突的问题。
- BigInt 是一种数字类型的数据,它可以表示任意精度格式的整数,使用 BigInt 可以安全地存储和操作大整数,即使这个数已经超出了 Number 能够表示的安全整数范围。
注意:NaN不是数据类型
typeof NaN === 'number' //true
NaN == NaN //false
2-2 对象类型(引用类型),有以下3种:
A.内置对象/原生对象
String Number Boolean Array Date RegExp Math Error Object Function Global
B.宿主对象
- (1)BOM对象: Window、Navigator、Screen、History、Location
- (2)DOM对象:Document、Body、Button、Canvas等
C.自定义对象--(指由用户创建的对象,兼容性问题需要由编写者注意)
创建自定义对象几种方式:
(1)对象直接量:
var obj1 = {};
var obj2 = {x:0,y:0};
var obj3 = {name:‘Mary’,age:18}
(2)工厂模式--用函数来封装以特定接口创建对象的细节:
function createPerson(name,age,job){
var o = new Object();
o.name = name;
o.age = age;
o.job = job;
return o;
}
var person1 = createPerson('zhang',30,'java');
(3)构造函数模式:
function Person(name,age,job){
this.name= name;
this.age = age;
this.job = job;
}
var person1 = new Person('zhang',30,'java');
(4)原型模式:
function Person(){}
Person.prototype.name = 'zhang';
Person.prototype.age = '22';
Person.prototype.job = 'html5';
var person1 = new Person();
2-3 数据类型的判断:
2-3-1:typeof
一般通过typeof操作符来判断一个值属于哪种基本类型
缺点:无法分辨对象类型
typeof 'seymoe' // 'string'
typeof true // 'boolean'
typeof 10 // 'number'
typeof Symbol() // 'symbol'
typeof null // 'object' 无法判定是否为 null
typeof undefined // 'undefined'
typeof {} // 'object'
typeof [] // 'object'
typeof(() => {}) // 'function'
为什么typeof null 为object:
js在底层储存变量的时候,会在变量的机器码的低位1-3位储存其类型信息
- 000:对象
- 010:浮点数
- 100:字符串
- 110: 布尔值
- 1:整数
但是,对于undefined 和 null 来说,这两个值得信息存储是有点特殊的。
- null:所有机器码均为0
- undefined:用 −2^30 整数来表示
所以,typeof在判断null的时候问题就出现了,由于null的所有机器码均为0,因此直接被当做了对象来看待。
2-3-2:instanceof
判断对象类型:测试构造函数的 prototype 是否出现在被检测对象的原型链上。
缺点:无法判断一个值到底属于数组还是普通对象
[] instanceof Array // true
({}) instanceof Object // true
(()=>{}) instanceof Function // true
let arr = []
let obj = {}
arr instanceof Array // true
arr instanceof Object // true
obj instanceof Object // true
在这个例子中,arr 数组相当于 new Array() 出的一个实例,
所以 arr.__proto__ === Array.prototype,
又因为 Array 属于 Object 子类型,
即 Array.prototype.__proto__ === Object.prototype,
所以 Object 构造函数在 arr 的原型链上
判断不了原始类型
console.log(true instanceof Boolean);// false
console.log(undefined instanceof Object); // false
console.log(arr instanceof Array); // true
console.log(null instanceof Object); // false
console.log({} instanceof Object); // true
console.log(function(){} instanceof Function);// true
2-3-3: Object.prototype.toString.call()
全能型,几乎都能判断
Object.prototype.toString.call({})// '[object Object]'
Object.prototype.toString.call([])// '[object Array]'
Object.prototype.toString.call(() => {})// '[object Function]'
Object.prototype.toString.call('abc')// '[object String]'
复制代码
传入原始类型却能够判定出结果是因为对值进行了包装。
「那么,什么是包装对象:」
所谓“包装对象”,指的是与数值、字符串、布尔值分别相对应的Number、String、Boolean三个原生对象。这三个原生对象可以把原始类型的值变成(包装成)对象。
3 js运算符:
3-1 delete 运算符
delete 运算符用来删除对象属性
或者数组元素
,如果删除成功
或所删除的目标不存在
,delete 将返回 true
。 然而,并不是所有的属性都可删除:
- 一些内置核心和客户端属性是不能删除的
- 通过 var 语句声明的变量不能删除
- 通过 function 语句定义的函数也是不能删除的。
例如:
var o = { x: 1, y: 2}; // 定义一个对象
console.log(delete o.x); // true,删除一个属性
console.log(delete o.x); // true,什么都没做,x 在已上一步被删除
console.log("x" in o); // false,这个属性在对象中不再存在
console.log(delete o.toString); // true,什么也没做,toString是继承来的
console.log(delete 1); // true,无意义
var a = [1,2,3]; // 定义一个数组
console.log(delete a[2]); // true,删除最后一个数组元素
console.log(2 in a); // false,元素2在数组中不再存在
console.log(a.length); // 3,数组长度并不会因 delete 而改变
console.log(a[2]); // undefined,元素2所在的位置被空了出来
console.log(delete a); // false,通过 var 语句声明的变量不能删除
function f(args){} // 定义一个函数
console.log(delete f); // false,通过 function 语句声明的函数不能删除
复制代码
3-2 void 运算符
void 运算符可以应用于任何表类型的表达式,表达式会被执行,但计算结果
会被忽略并返回undefined
。
例如:
void 0;
void "you are useless?";
void false;
void [];
void /(useless)/ig;
void function(){ console.log("you are so useless?"); }
void alert(1)
// always return undefined
复制代码
3-3 ++ -- 运算符
++ -- 递增递减运算符借鉴自 C 语言,它们分前置型和后置型,作用是改变一个变量的值。
例如:
var a = 5;
console.log(a++); // 5 后加表不加
console.log(a); // 6
console.log(++a); // 7 先加,都有加
console.log(a) // 7
console.log(a--); // 7
console.log(a) // 6
console.log(--a); // 5
console.log(a) // 5
复制代码
奇淫技巧: 先家都有家,后家表不家
(加号在前面,本身和表达式都加1;加号在后面,表达式不加1,本身加1 ),减法同理。
3-4 valueOf
var a = '你好', b = 1, c = [], d = {}, e = function (){}
a.valueOf() // '好'
b.valueOf() // 1
c.valueOf() //[]
d.valueOf() // {}
e.valueOf() //ƒ (){}
复制代码
3-5 +和-
"+" 操作符,如果有一个为字符串,那么都转化到字符串然后执行字符串拼接
"-" 操作符,转换为数字,相减 (-a, a * 1 a/1) 都能进行隐式强制类型转换
[] + {} // "[object Object]"
{} + [] // 0
1 + true //2
1 + false //1
4 内存
4-1 执行上下文
当代码运行时,会产生一个对应的执行环境,在这个环境中,所有变量会被事先提出来(变量提升),有的直接赋值,有的为默认值 undefined,代码从上往下开始执行,就叫做执行上下文。
「执行环境有三种」:
- 1.全局环境:代码首先进入的环境
- 2.函数环境:函数被调用时执行的环境
- 3.eval函数
「执行上下文特点:」
- 1.单线程,在主进程上运行
- 2.同步执行,从上往下按顺序执行
- 3.全局上下文只有一个,浏览器关闭时会被弹出栈
- 4.函数的执行上下文没有数目限制
- 5.函数每被调用一次,都会产生一个新的执行上下文环境
「执行3个阶段:」
1.创建阶段
-
(1).生成变量对象
-
(2).建立作用域链
-
(3).确定 this 指向
2.执行阶段
-
(1).变量赋值
-
(2).函数引用
-
(3).执行其他代码
3.销毁阶段
- (1).执行完毕出栈,等待回收被销毁
4-2 堆栈
「概念:」
-
栈: 栈会
自动分配
内存空间,它由系统自动释放
;存放基本
类型,简单的数据段,占据固定
大小的空间 -
堆:
动态分配
的内存,大小不定也不会自动释放
。存放引用
类型,那些可能由多个值构成的对象
,保存在堆内存中
5 垃圾回收机制
MDN上有说: 从2012年起,所有现代浏览器都使用了
标记-清除垃圾
回收算法。所有对于js垃圾回收算法的改进都是基于标记-清除算法的改进
「什么是垃圾:」 一般来说,没有
被引用
的对象就是垃圾,就是要才清除的。但有个例外,如果几个对象相互引用形成一个环,但根访问不到他们,他们也是垃圾(引用计数法,无法清除他们)
「垃圾回收的几种算法:」
5-1引用计数法
概念: 记录有多少“程序”在引用自己,当引用的数值为0时,就开始清除它。
优势:
- 可
马上
回收垃圾,当被引用数值为0
时,对象马上会把自己作为空闲空间
连到空闲链表
上,也就是说。在变成垃圾的时候就立刻
被回收。 - 因为是即时回收,那么‘程序’不会暂停去单独使用很长一段时间的GC,那么
最大暂停
时间很短
。 - 不用去遍历堆里面的所有活动对象和非活动对象
劣势:
- 计数器需要
占很大
的位置
,因为不能预估被引用的上限,打个比方,可能出现32位即2的32次方个对象同时引用一个对象,那么计数器就需要32位。 - 最大的劣势是
无法解决循环引用
无法回收的问题 这就是前文中IE9之前出现的问题
5-2 标记清除法
主要将GC的垃圾回收过程分为两个阶段
标记阶段:把所有活动
对象做上标记
。
清除阶段:把没有
标记(也就是非活动对象)销毁
。
优势:
- 实现简单,打标记也就是打或者不打两种可能,所以就一位二进制位就可以表示
- 解决了循环引用问题
缺点
- 造成碎片化(有点类似磁盘的碎片化)
- 再分配时遍次数多,如果一直没有找到合适的内存块大小,那么会遍历空闲链表(保存堆中所有空闲地址空间的地址形成的链表)一直遍历到尾端
-
5-3 复制算法
- 将一个内存空间分为两部分,一部分是
From
空间,另一部分是To
空间 - 将
From空间
里面的活动
对象复制
到To空间
- 释放掉整个
From
空间 - 再将From空间和To空间的身份
互换
,那么就完成了一次GC。
6 内存泄漏
「概念:」 申请的内存
没有及时回收
掉,造成系统内存
的浪费
,导致程序运行
速度减慢
甚至系统崩溃等严重后果
「内存泄漏发生的场景:」
(1) 意外的全局变量
function leaks(){
leak = 'xxxxxx';//leak 成为一个全局变量,不会被回收
}
复制代码
(2) 遗忘的定时器
setTimeout 和 setInterval 是由浏览器专门线程
来维护
它的生命周期
,如果在某个页面使用了定时器,当销毁页面时,没有手动去释放清理这些定时器的话,那么这些定时器还是存活着的
(3) 使用不当的闭包
var leaks = (function(){
var leak = 'xxxxxx';// 被闭包所引用,不会被回收
return function(){
console.log(leak);
}
})()
复制代码
(4) 遗漏的 DOM 元素
<div id="container">
</div>
$('#container').bind('click', function(){
console.log('click');
}).remove();//dom移除了,但是js还持有对它的引用
复制代码
解决:
$('#container').bind('click', function(){
console.log('click');
}).off('click').remove();
//把事件清除了,即可从内存中移除
复制代码
(5) 网络回调
「如何监控内存泄漏」
使用控制台
详细点击这里
7 作用域
扩展:
JavaScript是门动态语言,跟Java不一样,JavaScript可以随意定义全局变量和局部变量,每一个函数都是一个作用域,当函数执行时会优先查找当前作用域,然后逐级向上。
JavaScript是静态作用域,在对变量进行查询时,变量值由函数定义时的位置决定,和执行时的所处的作用域无关。
ES6已经有块级作用域了,而且用 let 和 const 定义的变量不会提升。
概念:
作用域:变量或者函数的有效作用范围
作用域链:我们需要查找某个变量值,会先在当前作用域查找,如果找不到会往上一级查,如果找到的话,就返回停止查找,返回查找的值,这种向上查找的链条关系,叫作用域
7-1 相关案例
(1)变量提升/变量由函数定义时的位置决定
var a = 1;
function fn() {
console.log('1:' + a);
var a = 2;
bar()
console.log('2:' + a)
}
function bar() {
console.log('3:' + a)
}
fn()
复制代码
分别打印:1:undefined 3:1 2:2
「解析:」
第一个 a 打印的值是 1:undefined 而不是 1。因为我们在 fn() 中定义了变量 a,用 var 定义的变量会提升到当前作用域的顶部(即当前函数作用域顶部),但是赋值还在原地,所以是undefined。
第二个a 打印的值是 3:1 而不是 2。因为函数 bar 是定义在全局作用域中的,所以作用域链是 bar -> global,bar 里面没有定义a,所以就会顺着作用域链向上找,然后在 global 中找到了 a。注意:查找是在其定义
的执行上下文环境中查找。
第三个 a 打印的值是 2:2。这句话所在的作用域链是 fn -> global,执行 console.log('2:' + a) 会首先在 fn 作用域里查找 a,找到有 a,并且已经赋值为2,所以结果就是2。
(2)变量赋值
var a = 1;
function fn() {
console.log('1:' + a);
a = 2
}
a = 3;
function bar() {
console.log('2:' + a);
}
fn();
bar();
复制代码
分别打印:1:3 2:2
「解析:」
第一个 打印的值是 1:3。首先, fn 中的 a = 2 是给变量 a 赋值,并不是声明变量。然后,执行函数 fn,此时 a 已经赋值为3了,注意,fn()是在a=3后面执行。
第二个 打印的值是 2:2。函数 bar 所能访问的作用域链为 bar->global,在执行函数 bar 时,由于在bar前执行了fn()将a修改为2了,所以这个时候拿到的a为2。
(3)全局变量声明提前
if(!(a in window)){
var a = 10;
}
console.log(a);
复制代码
打印:undefined
「解析:」
相当于:
var a;
if(!(a in window)){
a = 10;
}
console.log(a);
复制代码
用 var 定义的变量会提升到当前作用域的顶部(即当前全局作用域), 所以a会声明提前到window中,但值还是在原地,即为undefined。 所以if得到是a in window是ture 故不走里面赋值 console.log(a) == undefined
上一个例子的变种:
(function(){
var x = c = b = {a:1}
})()
console.log(c,b) // {a: 1} {a: 1}
console.log(x.a); // error , x is not defined
复制代码
注意: x是在函数中声明的,是局部变量,c和b未声明,直接赋值,所以是全局变量。 赋值过程是从右往左的,即b={a:1},c=b,x=c
(4)变量提升/运算符顺序
(function(){
var a = b = 3;
})()
console.log(typeof a === "undefined"); // true
console.log(typeof b === "undefined"); // false
console.log(typeof b === "number" && b ===3); // true
复制代码
// 这里涉及的就是立即执行和闭包的问题,还有变量提升,运算符执行方向(=号自右向左)
// 那个函数可以拆成这样
(function()
var a; /* 局部变量,外部没法访问*/
b = 3; /* 全局变量,so . window.b === 3 , 外部可以访问到*/
a = b;
})()
复制代码
(5)变量提升/运算符顺序
var x = 1;
if (function f(){console.log(2)}) {
x += typeof f;
}
console.log(x); // 1undefined
复制代码
//因为函数体在()中会以表达式去运行,fn函数不起作用,函数不会执行。
//最后表达式转换为true,f未声明(上面的函数没起作用),值为undefined
「知识点:」
(1) 在JavaScript中,通过 let 和 const 定义的变量具有块级作用域的特性。
(2) 通过 var 定义的变量会在它自身的作用域中进行提升,而 let 和 const 定义的变量不会。
(3) 每个JavaScript程序都具有一个全局作用域,每创建一个函数都会创建一个作用域。
(4) 在创建函数时,将这些函数进行嵌套,它们的作用域也会嵌套,形成作用域链,子作用域可以访问父作用域,但是父作用域不能访问子作用域。
(5) 在执行一个函数时,如果我们需要查找某个变量值,那么会去这个函数被 定义 时所在的作用域链中查找,一旦找到需要的变量,就会停止向上查找。
(6) “变量的值由函数定义时的位置决定”
这句话有歧义,准确说是查找变量时,是去定义这个函数时
所在的作用域链查找。
8 闭包
「概念:」
一个function
里面return了一个子函数
,子函数
访问了父函数的变量。
「应用场景:」
- 函数防抖
- 封装私有变量
- 解决for循环变量相互影响的问题
目标:想要依次打印0-9
for(var i = 0; i < 10; i++){
setTimeout(()=>console.log(i),0)
}
// 实际控制台输出了10遍10.
复制代码
用闭包解决
for(var i = 0; i < 10; i++){
(function(a){
setTimeout(()=>console.log(a),0)
})(i)
}
// 控制台输出0-9
复制代码
闭包是如何产生的
当前作用域产产生了对父作用域的引用
JavaScript代码的整个执行过程,分为两个阶段,代码编译阶段
与代码执行阶段
。编译阶段由编译器完成,将代码翻译成可执行代码,这个阶段作用域规则
会确定。执行阶段由引擎完成,主要任务是执行可执行代码
,执行上下文在这个阶段创建。
9 this
9-1 this的指向:
ES5中:
this 永远指向最后调用
它的那个对象
ES6箭头函数:
箭头函数的 this 始终指向函数定义时
的 this,而非执行时
。
9-2 怎么改变this的指向:
- 使用 ES6 的箭头函数
- 在函数内部使用 _this = this
- 使用 apply、call、bind
- new 实例化一个对象
案例1:
var name = "windowsName";
var a = {
name : "Cherry",
func1: function () {
console.log(this.name)
},
func2: function () {
setTimeout( function () {
this.func1()
},100);
}
};
a.func2() // this.func1 is not a function
复制代码
在不使用箭头函数的情况下,是会报错的,因为最后调用 setTimeout 的对象是 window,但是在 window 中并没有 func1 函数。可以看做window.setTimeout
案例2:
var webName="long";
let func=()=>{
console.log(this.webName);
}
func();//long
复制代码
//箭头函数在全局作用域声明,所以它捕获全局作用域中的this,this指向window对象
案例3:
var webName = "long";
function wrap(){
let func=() => {
console.log(this.webName);
}
func();
}
wrap();//long
复制代码
//wrap函数执行时,箭头函数func定义在wrap中,func会找到它最近一层非箭头函数的this
//也就是wrap的this,而wrap函数作用域中的this指向window对象。
9-3 箭头函数:
「Tips:」 众所周知,ES6 的箭头函数是可以避免 ES5 中使用 this 的坑的。箭头函数
的 this 始终指向函数定义时
的 this,而非执行时
。
箭头函数需要记着这句话:“箭头函数中没有 this 绑定,必须通过查找作用域链来决定其值(箭头函数本身没有this,但是在它声明时可以捕获别人的this供自己使用。),如果箭头函数被非箭头函数包含,则 this 绑定的是最近一层非箭头函数的 this,否则,this 为 undefined”。
特点:
- 没有this
- 没有arguments
- 不能通过new关键字调用
- 没有new.target
- 没有原型
- 没有super