JavaScript学习笔记2
一、JS 基础的深入
1、数据
数据类型判断
判断数据类型的方法有 typeof、instanceof、===
typeof 返回的是字符串类型的数据类型表达
— 可以判断:undefined、数值、字符串、布尔值、函数
— 不能判断:null 与 object(因为都会返回’object’)、object 与 array
instanceof 用于判断对象的具体类型(数组、函数等)
=== 可以判断:undefined、null(因为这两种数据类型只有一种值)
var a1 = {
a2:[1,'a',console.log],
a3:function(){}
}
a1 instanceof Object //true
a1.a2 instanceof Object //true
a1.a2 instanceof Array //true
a1.a3 instanceof Object //true
a1.a3 instanceof Function //true
typeof a1.a3 === 'function' //true
undefined 和 null 的区别
undefined 代表定义了未赋值,null 表示定义并赋值了,值为null
什么时候为变量赋值为 null
初始赋值为 null,表明其将要赋值为对象
结束前赋值为 null 为了释放对象在内存中的位置,让变量指向的对象成为垃圾对象(被垃圾回收器回收)
变量类型与数据类型
数据类型分为基本类型和对象类型,变量的类型(变量内存值的类型)分为基本类型(保存的是基本类型数据)和引用类型(保存的是地址值)
2、数据、变量与内存
什么是数据
存储在内存中代表特定信息的东西,本质上是 01 串
数据的特点:可传递,可运算
一切皆数据,函数也是数据
内存中所有操作的目标是数据,如算术运算、逻辑运算、赋值、运行函数
什么是内存
内存条通电后产生的可存储数据的临时空间
内存的产生和死亡:内存条(电路板)–> 通电 –> 产生内存空间 –> 存储数据 –> 处理数据 –> 断电 –> 内存空间和数据消失
分配内存:声明变量和函数或创建对象时,JS 引擎会自动分配一定大小的内存来存放对应数据
释放内存:清空内存中的数据,表示内存可以再分配使用(内存不释放就不能复用)
一块内存有两个数据:内部存储的数据和地址值
内存分类:栈和堆
— 栈:存全局变量、局部变量、标识对象的变量(如函数名),空间较小
— 堆:对象(如数组、函数),空间较大
什么是变量
变量是可变化的量,由变量名和变量值组成
每个变量都对应一块小内存,变量名用来查找对应的内存,变量值就是内存中保存的数据
数据、内存、变量之间的关系
内存是一个容器,用来存储程序运行需要操作的数据的临时空间
变量(变量名、变量值)是内存的标识,通过变量找到对应内存,进而操作(读/写)内存中的数据
var a = xxx, a 内存中保存的是什么?
(1)xxx 是基本数据类型,a 保存的是这个数据
(2)xxx 是对象,保存的是对象的地址值
(3)xxx 是一个变量,保存的是 xxx 的内存内容(可能是基本数据类型,也可能是地址值)
var b = 'abc'
var a = b //a 保存的是 'abc'
b = {}
a = b //a 保存的是 b 中的内存内容,只是 b 的内存内容存的对象地址值
引用变量赋值
n 个引用变量指向同一个对象,通过一个变量修改对象内部数据,其他所有变量看到的是修改之后的数据
n 个引用变量指向一个对象,让其中一个引用变量指向另一个对象,其他所有引用变量依然指向前一个对象
var a = {age:10}
var b = a
b.age = 12 //此时 a.age 也变为 12
function fn(obj){
obj.age = '15'
}
fn(a) //此时 a.age 和 b.age 都变为 15
var c = {age:12}
var d = c
c = {name:'BOB',age:13} //c 相当于重新开辟了新的空间创建了个新的对象,此时 c 和 d 指向两个对象,d.age 还是 12
var e = {age:12}
function fun(obj){
obj = {age:15} //相当于创建一个新对象,obj 存的地址改变
}
fun(e) // 此时 e.age 还是12,函数中的 obj 会变成垃圾被回收
在 JS 调用函数时传递变量参数时,是值传递还是引用传递
理解1:都是值(包括基本值或地址值)传递
理解2:可能是值传递,也可能是引用传递(即传地址值,注意不是地址,而是表示地址的值)
var a = 3
function fn(a){
a = a + 1
}
console.log(a) // a 还是 3
function fn2(obj){
console.log(obj.name)
}
var obj = {name:'tom'}
fn2(obj)
JS 引擎如何管理内存
(1)内存生命周期
— 分配小内存空间,等到它的使用权
— 存储数据,可以对它反复进行操作
— 释放小内存空间,清空内存中数据,内存可再被分配复用
(2)释放内存
局部变量(为执行函数分配的栈空间内存):函数执行完自动释放
对象(存储对象的堆空间内存):先成为垃圾对象(即先把指向对象的变量设为 null,即内存没有引用指向时) —> 由垃圾回收器回收
全局变量在运行时不会释放
var a = 3
var obj = {} //此时共占用 3 个小内存,分别被 a、obj、obj 所指向对象占用
obj = null //此时 obj 指向的对象成为垃圾对象,而 obj 变量所占内存没有释放
3、对象
什么是对象
多个数据的封装体(集合体),用来保存多个数据的容器
一个对象代表现实中的一个事物
对象类型的数据有 Object、Function(可执行)、Array(内部数据有序)
为什么要用对象
统一管理多个数据
对象的组成
对象由属性和方法组成
属性:由属性名(字符串)和属性值(任意类型)组成
方法:是一种特别的属性,属性值是函数的属性
如何访问对象内部数据
.属性名
:编码简单,有时不能用
['属性名']
:编码复杂,能通用
什么时候必须使用[‘属性名’]的方式获取属性
— 属性名包含特殊字符:- 空格
— 使用变量表示属性名
4、函数
什么是函数
实现特定功能的 n 条语句的封装体
只有函数是可执行的,其他类型的数据不能执行
函数也是对象
为什么要用函数
提高代码复用
便于阅读交流
如何定义函数
— 函数声明 function fn(){}
— 表达式 var fn = function(){}
如何调用(执行)函数
test()
直接调用
对象.方法名()
通过对象调用
new 函数名()
new 调用
函数对象.call(对象实例)
或 函数对象.apply(对象实例)
让一个函数成为指定任意对象的方法进行调用
回调函数
什么函数才是回调函数
1)定义的 2)没有显式调用 3)但最终它在某个时刻或某个条件下执行了
常见的回调函数
— dom 事件回调函数(如 点击 onclick、获取焦点等),this 是发生事件的 dom 元素
— 定时器(如setTimeout)回调函数,this 是 window
— ajax 请求回调函数
— 生命周期回调函数
立即执行函数表达式 IIFE
IIFE 全称是 Immediately-Invoked Function Expression,可理解为匿名函数自调用
作用:隐藏内部实现,不会污染外部(全局)命名空间,可用它来编码 js 模块
;(function(){ //匿名函数自调用,前面的分号是为了防止上一行后没有分号把下面都连一块,所以使用分号分隔开
var a = 1;
function test(){
console.log(++a)
}
window.$ = function(){ //向外暴露一个全局函数
return{
test:test
}
}
})()
$().test() //输出 2
函数中的 this
this 是什么
任何函数本质上都是通过某个对象调用的,若没有直接指定就是window
所有函数内部都有一个变量 this
this 的值是调用函数的当前对象
如何确定 this 的值
函数名() this 是 window
对象实例.函数名() this 是对象实例
new 函数名() this 是新创建的对象
函数对象.call(对象实例) this 是参数中的对象实例
function Person(color){
console.log(this)
this.color = color
this.getColor = function(){
console.log(this)
}
}
Person('red') //this 是 window
var p = new Person('red') //this 是 p
p.getColor() //this 是 p
var obj = {}
p.getColor.call(obj) //this 是 obj
var test = p.getColor
test() //this 是 window
function fun1(){
function fun2(){
console.log(this)
}
fun2() //this 是 window
}
fun1() //this 是 window
5、JS 语句中的分号
是否加分号是编码风格的问题,看个人喜好(如 vue.js 框架源码中没用分号)
必须加分号的情况:
— 小括号开头的前一条语句
— 中括号开头的前一条语句
上面的情况以防万一可以在行首加分号
;(function(){})()
;[1,3].forEach(function(){})
二、函数高级
1、原型与原型链
函数的 prototype 属性
每个函数都有一个 prototype 属性(在函数定义时创建函数对象,并添加 prototype 属性 this.prototype = {}
)它默认指向一个 Object 空对象(没有我们指定的属性),prototype 指向的对象即称为原型对象
原型对象(即 prototype 所指向的)中有一个属性 constructor,它指向函数对象
Date.prototype.constructor === Date //true
function Fun(){}
Fun.prototype.constructor === Fun //true
Fun.prototype.test = function(){}
var fun = new Fun()
fun.test()
如上图构造函数(左边)和原型对象(右边)相互引用
给原型对象添加属性(一般都是方法)
作用:函数的所有对象实例自动拥有原型中的属性(方法)
显式原型与隐式原型
每个函数都有一个 prototype,即显式原型属性,在定义函数时自动添加的 this.prototype = {}
,默认指向一个空的 Object 实例对象(但是 Object 不满足)
每个实例对象都有一个 __proto__
,称为隐式原型属性,创建对象时自动添加的 this.__proto__ = 构造函数.prototype
,默认为构造函数的 ptototype 属性值
对象的隐式原型的值为其对应的构造函数的显式原型的值
程序员能直接操作显式原型,但不能直接操作隐式原型(ES6 之前)
function Fn(){} //定义构造函数,内部语句:this.prototype = {},this 指函数对象
Fn.prototype //显式原型属性
var fn = new Fn() //内部语句:this.__proto__ = Fn.prototype,this 指 fn
fn.__proto__ //隐式原型属性
Fn.prototype === fn.__proto__ //true
Fn.prototype.test = function(){}
fn.test()
(prototype 是给构造函数使用(读/写),__proto__
是给对象实例使用(读))
记住:实例对象的隐式原型 __proto__
= 构造函数的显式原型 prototype
原型链(别名隐式原型链)
访问一个对象属性时,先在自身属性中查找,找到返回;若没有,再沿着 __proto__
这条链向上查找,找到返回;若最终没有找到,返回 undefined
作用:查找对象的属性(方法)
function Fn = function(){
this.test1 = function(){}
}
Fn.prototype.test2 = function(){}
var fn = new Fn()
fn.test1()
fn.test2()
fn.toString()
fn.test3() //报错函数 not define
构造函数、原型、实例对象的关系
所有函数都是 Function 的实例(包含 Function)
所有函数的 __proto__
都是一样的,因为构造函数都是 Function()
function Foo(){} 等价于 var Foo = new Function()
注意 Function = new Function()
上图中 Foo 是 Function 的实例对象,Object 是 Function 的实例对象,Function 的实例对象和构造函数都是它自己,Function 的原型是 Object 的实例对象
Foo.prototype instanceof Object //true
Object.prototype instanceof Object //false
Function.prototype instanceof Object //true
原型链补充
(1)函数的显式原型指向的对象默认是空 Object 实例对象(但 Object 不满足)
Foo.prototype instanceof Object //true
Object.prototype instanceof Object //false
Function.prototype instanceof Object //true
(2)所有函数都是 Function 的实例(包含 Function)
Function.__proto__ === Function.prototype
(3)Object 的原型对象是原型链的尽头
Object.prototype.__proto__ //是 null
原型链的属性问题
读取对象属性值时,会自动到原型链中查找
设置对象的属性值时,不会查找原型链,若当前对象中没有该属性,则直接添加该属性
方法一般定义在原型中,属性一般通过构造函数定义在对象本身上
function Person(name,age){
this.name = name
this.age = age
}
Person.prototype.setName = function(name){
this.name = name
}
var p1 = new Person('aa',12)
p1.setName('bb') //此时 p1 实例中有两个属性 name(为 bb) 和 age
var p2 = new Person('cc',12)
p2.setName('dd') //此时 p2 实例中有两个属性 name(为 dd) 和 age
p1.__proto__ === p2.__proto__ //true
instanceof
A实例对象 instanceof B构造函数
若 B 函数的显式原型对象在 A 对象的(隐式)原型链上,返回 true,否则返回 false
Function 是通过 new 自己产生的实例
Object instanceof Function //true
Object instanceof Object //true
Function instanceof Function //true
Function instanceof Object //true
function Foo(){}
Object instanceof Foo //false
相关例子
function A(){}
A.prototype.n = 1
var b = new A()
A.prototype = {n:2,m:3}
var c = new A()
console.log(b.n, b.m, c.n, c.m) //1 undefined 2 3
function Foo(){}
Object.prototype.a = function(){}
Function.prototype.b = function(){}
var f1 = new Foo()
f1.a() //不报错
f1.b() //报错
Foo.a() //不报错
Foo.b() //不报错
2、执行上下文与执行上下文栈
变量提升与函数提升
变量声明提升:通过 var 定义的变量,在定义语句之前就可以访问到,只是值为 undefined
函数声明提升:通过 function 声明的函数,在声明语句之前就可以直接调用,值为函数对象
console.log(a) //输出 undefined,变量提升
fn1() //可调用,函数提升
fn2() //不能调用,变量提升
var a = 3
function fn1(){}
var fn2 = function(){}
变量提升和函数提升的产生过程:
执行上下文
代码根据其位置可分为全局代码和函数(局部)代码
执行上下文的个数 = 调用的函数数量 + 1(指 window)
全局执行上下文
(1)在全局代码前将 window 确定为全局执行上下文
(2)对全局数据进行预处理
— var 定义的全局变量=undefined,并添加为 window 的属性
— function 声明的全局函数创建好函数对象,并添加为 window 的方法
— this 赋值为 window
(3)开始执行全局代码
函数执行上下文
只有在调用函数的时候才会生成执行上下文
(1)在调用函数,准备执行函数体前,创建对应的函数执行上下文对象(封闭的虚拟对象,是栈内存中的一片区域,里面存放局部变量、形参、arguments等)
(2)对局部数据进行预处理
— 形参=实参,并添加为执行上下文的属性
— arguments=实参列表,并添加为执行上下文的属性
— var 定义的局部变量=undefined,并添加为执行上下文的属性
— function 声明的函数创建好函数对象,并添加为执行上下文的方法
— this 赋值为调用函数的对象
(3)开始执行函数体代码
function fn(a1){
console.log(a1) //2
console.log(a2) //undefined
a3() //a3()执行
console.log(this) //this 指 window
console.log(arguments) //伪数组[2,3]
var a2 = 3
function a3(){
console.log('a3()执行')
}
}
fn(2,3)
执行上下文栈
(1)在全局代码执行前,JS 引擎会创建一个栈来存储管理所有的执行上下文对象
(2)在全局执行上下文(window)确定后,将其添加到栈中(压栈)
(3)在函数执行上下文创建后,将其添加到栈中(压栈)
(4)在当前函数执行完成后,将栈顶的对象移除(出栈)
(5)当所有的代码执行完后,栈中只剩下 window
//1.进入全局执行上下文
var a = 10
var bar = function(x){
var b = 5
foo(x+b) //3.进入 foo 执行上下文
}
var foo = function(y){
var c = 5
console.log(a+c+y)
}
bar(10) //2.进入 bar 函数执行上下文
栈底一定是 window,当前运行的总是栈顶的上下文对象
相关例子
1
var a = 3
function fn(){
console.log(a)
var a = 4 //a 在 console.log 前声明 a,在此行才赋值
}
fn() //输出 undefined,因为在 fn 函数中
2
console.log('gb:'+i)
var i = 1
foo(1)
function foo(i){
if(i === 4){return}
console.log('fb:'+i)
foo(i+1)
console.log('fe:'+i)
}
console.log('ge:'+i)
代码依次输出 gb:undefined、fb:1、fb:2、fb:3、fe:3、fe:2、fe:1、ge:1,整个过程中产生了 5 个执行上下文
3
function a(){}
var a
console.log(typeof a) //输出 'function'
先执行变量提升,再执行函数提升
4
if(!(b in window)){ //此处判断为 !true 即 false
var b = 1
}
console.log(b) //输出 undefined
5
var c = 1
function c(c){
console.log(c)
var c =3
}
c(2) //此处会报错
上述代码会报错,因为上述代码相当于
var c
function c(c){
console.log(c)
var c =3
}
c = 1
c(2) //此处会报错
3、作用域与作用域链
作用域是一个代码所在的区域,它是静态的(执行上下文对象是在执行调用的时候才产生是动态的),在编写代码时就确定了
作用域分为全局作用域、函数作用域
没有块作用域(即在大括号内声明的变量在外面也可见,但 ES6 有,Java 中也有)
if(true){var c =3;} //大括号内的 c 在外面也可见,没有块作用域
console.log(c)
作用域的作用:隔离变量,不同作用域下同名变量不会冲突
作用域的个数 = 定义的函数数量 + 1(指全局)
作用域与执行上下文的区别
(1)全局作用域之外,每个函数都会创建自己的作用域,作用域在函数定义时就已经确定了,而不是在函数调用时
全局执行上下文环境是在全局作用域确定之后,js 代码马上执行之前创建
函数执行上下文是在调用函数时,函数体代码执行之前创建
(2)作用域是静态的,只要函数定义好就一直存在,且不会再变化
执行上下文是动态的,调用函数时创建,函数调用结束时就会自动释放
作用域与执行上下文的联系
执行上下文环境(对象)是从属于所在的作用域
全局上下文环境 ==> 全局作用域
函数上下文环境 ==> 对应的函数作用域
作用域链
作用域链是多个上下级关系的作用域形成的链,它的方向是从下向上的(从内到外),即嵌套的作用域产生的由内向外的链
作用域链作用:查找变量时就是沿着作用域链来查找的
查找一个变量的查找规则:
— 1.在当前作用域的执行上下文中查找对应的属性,若有直接返回,否则进入 2
— 2.在上一级作用域的执行上下文中查找对应的属性,若有直接返回,否则进入 3
— 3.再次执行 2 的相同操作,直到全局作用域,若还找不到就抛出找不到的异常
相关例子
var x = 10
function fn(){
console.log(x)
}
function show(f){
var x = 20
f()
}
show(fn) //会输出 10
上述代码会输出 10
var fn = function(){
console.log(fn)
}
fn() //会输出 fn 函数
上述代码会输出 fn 函数
var obj = {
fn2: function(){
console.log(fn2)
}
}
上述代码会报错 fn2 is not defined,因为在当前作用域先找 fn2 没有,再去外部全局作用域找也没有
var obj = {
fn2: function(){
console.log(this.fn2)
}
}
上述代码不会报错,会输出 obj 对象中的 fn2 方法
4、闭包
例子:分别点击三个按钮弹出相应的数字
var btns = document.getElementsByTagName('button')
for(var i = 0,length = btns.length; i < length; i++){
var btn = btns[i]
btn.onclick = function(){
alert('第'+(i+1)+'个')
}
}
上述代码在点击所有按钮时都是弹出 ‘第4个’
var btns = document.getElementsByTagName('button')
for(var i = 0,length = btns.length; i < length; i++){
var btn = btns[i]
btn.index = i //将 btn 所对应的下标保存在 btn 上
btn.onclick = function(){
alert('第'+(this.index+1)+'个')
}
}
上述代码可正常实现功能
var btns = document.getElementsByTagName('button')
for(var i = 0,length = btns.length; i < length; i++){
(function(i){
var btn = btns[i]
btn.onclick = function(){
alert('第'+(i+1)+'个')
}
})(i)
}
上述代码利用闭包实现功能
如何产生闭包
当一个嵌套的内部(子)函数引用了嵌套的外部(父)函数的变量(包括函数)时,就产生了闭包,如
function fn1(){
var a = 2
var b = 'abc'
function fn2(){ //闭包
console.log(a)
}
fn2()
}
fn1()
什么是闭包
理解1:闭包是嵌套的内部函数
理解2:包含被引用变量(函数)的对象
注意:闭包存在于嵌套的内部函数中
产生闭包的条件
(1)函数嵌套
(2)内部函数引用了外部函数的数据(变量/函数),并执行内部函数定义就会产生闭包(不用调用内部函数,但必须调用外部函数)
外部函数执行几次(内部函数就创建几次)就产生几个闭包,和内部函数执行几次没有关系
function fn1(){
var a = 2 //在此处打断点就已产生闭包,因为变量提升和函数提升,已执行 fn2 函数定义
var b = 'abc'
function fn2(){ //闭包
console.log(a)
}
fn2()
}
fn1()
function fn1(){
var a = 2 //在此处打断点就还未产生闭包,因为变量提升但 fn2 变量还未赋值为函数,所以函数定义未执行
var b = 'abc'
var fn2 = function(){ //闭包,闭包中有 a 没有 b
console.log(a)
}
fn2()
}
fn1()
常见的闭包
(1)将函数作为另一个函数的返回值
function fn1(){
var a = 2
var fn2 = function(){ //闭包
a++
console.log(a)
}
return fn2
}
var f = fn1()
f() //输出 3
f() //输出 4
上述代码只产生一次闭包,并且在调用 f() 时闭包没有消失
(2)将函数作为实参传递给另一个函数调用
function showDelay(msg,time){
setTimeout(function(){ //此处产生闭包,因为内部函数使用了外部函数的 msg,产生闭包的原因和 setTimeout 以及 time 变量无关
alert(msg)
},time)
}
showDelay('xxx',2000)
闭包的作用
(1)使用函数内部的变量在函数执行完后,仍然存活在内存中(延长了局部变量的生命周期)
(2)让函数外部可以操作(读写)到函数内部的数据(变量/函数)
function fn1(){
var a = 2
var fn2 = function(){
a++
console.log(a)
}
var fn3 = function(){
a--
console.log(a)
}
return fn3
}
fn1() //此时 fn1 内部的对象 fn2、fn3 都成为垃圾对象,局部变量也不存在了
var f = fn1() //fn1 执行完后 fn2 就不存在了,闭包中只有 a,fn3 本身也不存在,但 fn3 不成为垃圾对象,因为 f 指向了 fn3
f() //输出 1
f() //输出 0
总结:函数执行完后,函数内部声明的局部变量一般不存在,只有存在于闭包中的变量才可能存在(前提是闭包中的函数对象没有成为垃圾对象,被引用的局部变量才存在)
在函数外部不能直接访问函数内部的局部变量,但可以通过闭包让外部操作它
闭包的生命周期
产生:在嵌套内部函数定义执行完时就产生了(不是在调用时)
死亡:在嵌套的内部函数成为垃圾对象时
function fn1(){
//此时闭包就已经产生了(因为函数提升,内部函数对象已经创建了)
var a = 2
function fn2(){ //闭包
a++
console.log(a)
}
return fn2
}
var f = fn1()
f() //输出 3
f() //输出 4
f = null //此时闭包死亡(因为包含闭包的函数对象 fn2 成为垃圾对象)
function fn1(){
var a = 2
var fn2 = function(){
a++
console.log(a)
} //此时闭包才产生,因为此时才执行完函数定义
return fn2
}
var f = fn1()
f() //输出 3
f() //输出 4
f = null //此时闭包死亡(因为包含闭包的函数对象 fn2 成为垃圾对象)
闭包的应用
闭包可用于定义 JS 模块:
— 具有特定功能的 js 文件
— 将所有的数据和功能都封装在一个函数内部(私有的)
— 只向外暴露一个包含 n 个方法的对象或函数
— 模块的使用者只需通过模块暴露的对象调用方法来实现对应的功能
定义和使用模块的方式一:
定义一个模块 myModule.js
function myModule(){
//私有数据
var msg = 'xxx'
//操作数据的函数
function fn1(){ //闭包
console.log(msg.toUpperCase())
}
function fn2(){ //闭包
console.log(msg.toLowerCase())
}
//向外暴露对象(给外部使用的方法)
return {
fn1:fn1,
fn2:fn2
}
}
在另一个文件中
<script type="text/javascript" src="myModule.js"></script>
<script type="text/javascript">
var module = myModule()
module.fn1()
module.fn2()
</script>
定义和使用模块的方式二(这种方式更方便):
定义一个模块 myModule.js
(function(){
var msg = 'xxx'
//操作数据的函数
function fn1(){ //闭包
console.log(msg.toUpperCase())
}
function fn2(){ //闭包
console.log(msg.toLowerCase())
}
//向外暴露对象(给外部使用的方法)
window.module = { //通过 window 向外暴露
fn1:fn1,
fn2:fn2
}
})()
在另一个文件中
<script type="text/javascript" src="myModule.js"></script>
<script type="text/javascript">
module.fn1()
module.fn2()
</script>
定义和使用模块的方式三(这种方式有利于代码压缩,即把变量名自动转为单个字母表示):
定义一个模块 myModule.js
(function(w){
var msg = 'xxx'
//操作数据的函数
function fn1(){ //闭包
console.log(msg.toUpperCase())
}
function fn2(){ //闭包
console.log(msg.toLowerCase())
}
//向外暴露对象(给外部使用的方法)
w.module = { //通过 window 向外暴露
fn1:fn1,
fn2:fn2
}
})(window)
在另一个文件中
<script type="text/javascript" src="myModule.js"></script>
<script type="text/javascript">
module.fn1()
module.fn2()
</script>
闭包的缺点
闭包缺点:
(1)函数执行完后,函数内的局部变量没有释放,占用内存事件会变长
(2)容易造成内存泄露
解决:
(1)能不用闭包就不用
(2)及时释放,将指向内部函数的变量设为 null,让内部函数成为垃圾对象,进而回收闭包
内存溢出与内存泄露
(1)内存溢出:一种程序运行出现的错误,错误原因是当程序运行需要的内存超过了剩余的内存时,就会抛出内存溢出的错误
(2)内存泄露:占用的内存没有及时释放(但程序不出错),内存泄露积累多了就容易导致内存溢出
常见的内存泄露:
— 意外的全局变量
— 没有及时清理的计时器或回调函数
— 闭包
function fn(){
a = 3 //意外的全局变量
}
fn()
setInterval(function(){ //启动循环定时器后不清理
console.log('xxx)
},1000)
function fn1(){
var a =4
function fn2(){
console.log(++a)
}
return fn2
}
var f = fn1()
f()
相关例子
var name = "The window"
var obj = {
name:'My object'
fn:function(){
return function(){
return this.name;
}
}
}
alert(obj.fn()())
输出 The window,因为相当于直接调用 fn 里的匿名函数,上面代码中没有闭包,有函数嵌套,但内部函数没有引用外部函数变量
var name = "The window"
var obj = {
name:'My object'
fn:function(){
var that = this
return function(){
return that.name;
}
}
}
alert(obj.fn()())
输出 My object,直接调用 fn 里的匿名函数,但是输出的是 that 对象的 name,上面代码中有闭包,有函数嵌套,且内部函数引用外部函数变量 that
function fun(n,o){
console.log(o)
return{
fun:function(m){ //闭包,因为 n 引用了外部函数 fun 中的 n
return fun(m,n) //这里的 fun 函数是只最外面第一行的 fun 函数
}
}
}
var a = fun(0); //undefined,因为没有给形参 o 赋值,此时闭包里的 n 是 0
a.fun(1); //0,函数执行产生了新的闭包,但马上消失,因为没有变量来接收 a.fun(1) 返回值
a.fun(2); //0
a.fun(3); //0
var b = fun(0).fun(1).fun(2).fun(3); //undefined,0,1,2
var c = fun(0).fun(1); //undefined,0
c.fun(2); //1
c.fun(3); //1
三、面向对象高级
1、对象创建模式
方式一:构造函数
Object 构造函数(new Object())模式:先创建空 Object 对象,再动态添加属性/方法
适用场景:起始时不确定对象内部数据
问题:语句太多
方式二:对象字面量模式
使用 {} 创建对象,同时指定属性/方法
适用场景:起始时对象内部数据是确定的
问题:如果创建多个对象有,重复代码
方式三:工厂模式(较不常用)
通过工厂函数(返回一个对象的函数都可以称为工厂函数)动态创建对象并返回
适用场景:需要创建多个对象
问题:对象没有一个具体的类型,都是 Object 类型
function createPerson(name,age){
var obj = {
name:name,
age:age,
setName:function(name){
this.name = name
}
}
return obj
}
方式四:自定义构造函数模式
自定义构造函数,通过 new 创建对象
适用场景:需要创建多个类型确定的对象
问题:每个对象都有相同的数据,浪费内存
function Person(name,age){
this.name = name
this.age = age
this.setName = function(name){
this.name = name
}
}
方式五:构造函数+原型的组合模式
自定义构造函数,通过在函数中初始化,方法添加到原型上
适用场景:需要创建多个类型确定的对象
function Person(name,age){
this.name = name
this.age = age
}
Person.prototype.setName = function(name){
this.name = name
}
2、继承模式
方式一:原型链继承(得到方法)
步骤
— 定义父类型构造函数
— 给父类型的原型添加方法
— 定义子类型构造函数
— 创建父类型的实例对象赋值为子类型的原型
— 将子类型原型的构造属性设置为子类型
— 给子类型原型添加方法
— 创建子类型的对象:可以调用父类型的方法
//父类型
function Supper(){
this.supprop = 'supper'
}
Supper.prototype.showSupperProp = function(){
console.log(this.supprop)
}
//子类型
function Sub(){
this.subprop = 'sub'
}
//子类型的原型为父类型的一个实例对象
Sub.prototype = new Supper()
//让子类型的原型的 constructor 指向子类型
Sub.prototype.constructor = Sub //若没有这条语句则 constructor 指向 Supper
Sub.prototype.showSubProp = functioin(){
console.log(this.subprop)
}
var sub = new Sub()
sub.showSupperProp()
sub.showSubProp()
关键:子类型的原型为父类型的一个实例对象
方式二:借用构造函数继承(得到属性)
步骤:
— 定义父类型构造函数
— 定义子类型构造函数
— 在子类型构造函数中调用父类型的构造
function Person(name,age){
this.name = name
this.age = age
}
function Student(name,age,price){
Person.call(this,name,age) //相当于 this.Person(name,age)
this.price = price
}
var s = new Student('xx',12,10000)
关键:在子类型构造函数中通过 call() 调用父类型构造函数
方式三:组合继承
原型链 + 借用构造函数的组合继承
步骤:
— 利用原型链实现对父类型对象的方法继承
— 利用 call() 借用父类型构造函数初始化相同属性
function Person(name,age){
this.name = name
this.age = age
}
Person.protorype.setName = function(name){
this.name = name
}
function Student(name,age,price){
Person.call(this,name,age) //为了得到属性
this.price = price
}
Student.prototype = new Person() //为了能看到父类型原型的方法
Student.protorype.constructor = Student //为了修正 constructor 属性
Student.prototype.setPrice = function(price){
this.price = price
}
var s = new Student('xx',12,10000)
s.setName('yyy')
s.setPrice(12000)
console.log(s.name,s.age,s.price) //输出 yyy 12 12000
四、线程机制与事件机制
1、进程与线程
进程(process):程序的一次执行,占有独有的内存空间,各个应用程序间内存空间相互独立,可通过 Windows 任务管理器查看进程
线程(thread):线程是进程内一个独立的执行单元,是程序执行的一个完整流程,是 CPU的最小调度单元
进程负责为程序的运行提供必备的环境
(进程相当于工厂中的车间)
线程是计算机中的最小计算单位,线程负责执行进程中的程序
(线程相当于工厂的工人)
JS、浏览器是单线程
程序是单线程还是多线程是看一个进程内有多少线程
应用程序必须运行在某个进程的某个线程上
一个进程中至少有一个运行的线程,主线程,进程启动后自动创建
一个进程中可以同时运行多个线程,会说程序是多线程运行的
一个进程内的数据可以供其中的多个线程直接共享
多个进程之间的数据是不能直接共享的
线程池(thread pool):保存多个线程对象的容器,实现线程对象的反复利用
多进程与多线程
多进程运行:一个应用程序可以同时启动多个实例运行
多线程:在一个进程内,同时有多个线程运行
CPU 的核数可对应同时运行的线程数
比较单线程与多线程
多线程
优点:
能有效提升 CPU 利用率
缺点:
(1)创建多线程开销
(2)线程间切换开销
(3)死锁与状态同步问题
单线程
优点:顺序编程简单易懂
缺点:效率低
js 是单线程运行的,但使用 H5 中的 Web Workers 可以多线程运行
浏览器是多线程运行的
浏览器有单进程和多进程,单进程的有 Firefox、老版 IE,多进程的有 chrome、新版 IE,可通过在浏览器打开多个标签后在任务管理器中查看是单进程还是多进程
2、浏览器内核
浏览器内核:支撑浏览器运行的最核心的程序
不同浏览器的内核不同
— Chrome、Safari:webkit
— Firefox:Gecko
— IE:Trident
— 360、搜狗等国内浏览器:Trident + webkit
内核组成模块
主线程中:
— js 引擎模块(也是程序):负责 js 程序的编译(运行前动态编译)与运行,因此 js 代码在主线程中运行(使用了 Web Worker 另说)
— html,css 文档解析模块:负责页面文本的解析(读文本-按一定规则拆解)
— DOM/CSS 模块:负责 DOM/CSS 在内存中的相关处理(把标签转换为对象,页面在内存中是个 DOM 对象树)
— 布局和渲染模块:负责页面的布局和效果的绘制(内存中的对象)
……
分线程中:
— 定时器模块:负责定时器的管理
— DOM 事件响应模块:负责事件的管理
— 网络请求模块:负责 ajax 请求
3、定时器引发的思考
定时器真的是定时执行的吗?
定时器并不能保证真正定时执行
一般会延迟一点(可以接受),也可能延迟很长时间(不能接受)
若在主线程执行了一个长时间的操作,可能导致延时才处理
定时器回调函数是在分线程中执行的吗?
定时器回调函数是在主线程中执行的,js 是单线程的,所有 js 代码都是在主线程中执行(无论是回调函数还是非回调函数)
定时器是如何实现的?
事件循环模型
4、JS 是单线程执行的
js 是单线程执行的,回调函数也是在主线程,H5 提出了实现多线程的方案(Web Worker)
只有主线程能更新界面
证明 js 执行是单线程的
setTimeout() 的回调函数是在主线程执行的,定时器回调函数只有在运行栈中的代码全部执行完后才有可能执行
setTimeout(function(){
console.log('22')
},2000)
setTimeout(function(){
console.log('11')
},1000)
function fn(){console.log('fn')}
fn()
console.log('alert之前')
alert('----') //暂停当前主线程执行,同时暂停计时,点击确定后恢复程序执行和计时
console.log('alert之后')
为什么 js 要用单线程模式,而不用多线程模式
JavaScript 的单线程与它的用途有关,作为浏览器脚本语言,JavaScript 的主要用途是与用户互动,以及操作 DOM,这决定了它只能是单线程,否则会带来很复杂的同步问题(如同时对一个对象更新和删除)
代码分类
代码分类:初始化代码、回调代码
初始化执行代码(同步代码):包含绑定 DOM 事件监听,设置定时器,发送 ajax 请求的代码
回调执行代码(异步代码):处理回调逻辑
js 引擎执行代码的基本流程
(1)先执行初始化代码:包含一般代码和以下一些特别的代码
— 设置定时器
— 绑定监听
— 发生 ajax 请求
上述三种情况都包含回调函数,回调函数是异步执行
(2)后面在某个时可才会执行回调代码
setTimeout(function(){
console.log('22')
},0)
console.log('alert之前')
alert('----') //暂停当前主线程执行,同时暂停计时,点击确定后恢复程序执行和计时
console.log('alert之后')
输出
alert之前
弹出----
alert之后
22 //即使 setTimeout 的时间设为 0,回调函数也在初始化代码都执行结束后才会执行
5、 浏览器的事件循环(轮询)模型
模型的两个重要组成部分:
— 事件(定时器/DOM 事件/Ajax)管理模块
— 回调队列
事件驱动模型(event-driven interaction model)
如下图所示
如上图所示最左侧的栈中表示初始化代码执行,栈中是一些执行上下文对象,回调函数交给右侧的不同管理模块处理,右侧表示在分线程上执行的不同管理模块,各模块不由 js 引擎执行(在主线程执行),而是由浏览器负责,管理模块把回调函数插入下方队列待执行
模型运转流程
(1)执行初始化代码,将事件回调函数交给对应模块管理
(2)当事件发生时,管理模块会将回调函数及其数据添加到回调队列中
(3)只有当初始化代码执行完后,才遍历读取队列中的回调函数执行
执行栈(execution stack)
所有代码都在此空间中执行
浏览器内核(browser core)
js 引擎模块(在主线程处理)
其他模块(在主/分线程处理)
回调队列
callback queue 也称为任务队列(task queue)/消息队列(message queue)/事件队列(event queue)
事件轮询(event loop)
从任务队列中循环取出回调函数放入执行栈中一个接一个处理
请求响应模型(request-response model)
浏览器向服务器发送请求,服务器接收请求,并处理请求,并返回响应数据,浏览器接收响应数据并渲染
6、H5 Web Workers(多线程)
H5 规范提供了 js 分线程的实现,名为 Web Workers,是一个 JavaScript 多线程解决方案(js 原本是单线程运行的)
这样可以将一些大计算量的代码交由 Web Worker 给分线程运行而不冻结用户界面,但是子线程完全受主线程控制,且不得操作 DOM,所以这个新标准并没有改变 JavaScript 单线程的本质
相关API
Worker:构造函数,加载分线程执行的 js 文件
Worker.prototype.onmessage:用于接收另一个线程的回调函数
Worker.prototype.postMessage:向另一个线程发送消息
使用
创建在分线程执行的 js 文件
在主线程中的 js 中发消息并设置回调
主线程中
//创建一个 Worker 对象并向它传递将在新线程中执行的脚本的 URL
var worker = new Worker('worker.js')
//绑定接收消息的监听,主线程接收 worker 分线程传来的数据函数
worker.onmessage = function(event){ //也可写在 postMessage 后面,因为只有在初始化代码执行完后才有可能执行回调函数
console.log(event.data)
}
//主线程向 worker 分线程发送数据
woreker.postMessage("hello world")
分线程代码文件中
var onmessage = function (event){ //不能用函数声明
var msg = event.data //通过 event.data 获得发送来的数据
postMessage('ok') //将获取到的数据发送回主线程
}
分线程中的全局变量不是 window,而是它自己的全局变量,所以在分线程中无法操作 DOM 元素(document相关)不能更新界面,也无法使用 alert(window 的方法)等
不足
(1)慢,因为增加了消息传输
(2)Worker 内代码不能操作 DOM(更新 UI),因为全局对象不是 window
(3)不能跨域加载 JS
(4)不是每个浏览器都支持这个新特性
五、其他
1、定义在对象中的属性变量不是私有的,因为外部可直接操作,只有定义在函数内的变量才可能是私有的(闭包另说),因为函数有作用域
2、找变量是通过作用域链,找对象的属性是通过原型链
3、变量找不到会报错,属性找不到会返回 undefined,如找变量 a 找不到会报错,找 window.a 找不到会返回 undefined
4、new 一个对象背后做了什么?
— 创建一个空对象
— 给对象设置 __proto__
,值为构造函数对象的 prototype 属性值(即 this.__proto__ = Fn.prototype
)
— 执行构造函数体(给对象添加属性/方法)