文章目录
基础总结深入
1.11
引用类型
- Object: 任意对象
- Function: 一种特别的对象(可以执行)
- Array: 一种特别的对象(数值下标,内部数据是有序的)
判断数据类型
三种方法判断数据类型:typeof、instanceof、===
- typeof:
可以判断: undefined/ 数值 / 字符串 / 布尔值 / function
不能判断: null与object object与array - instanceof:
判断对象的具体类型 - ===
可以判断: undefined, null
typeof的返回值是一个字符串,如果要检查一个变量a是不是undefined,要用typeof判断,应该写console.log(typeof(a) === ‘undefined’)
typeof(null)的值是object,因此如果要检查一个变量是不是null,只能用===
var b1 = {
b2: [1, 'abc', console.log],
b3: function () {
console.log('b3')
return function () {
return 'xfzhang'
}
}
}
console.log(b1 instanceof Object, b1 instanceof Array) // true false
console.log(b1.b2 instanceof Array, b1.b2 instanceof Object) // true true
console.log(b1.b3 instanceof Function, b1.b3 instanceof Object) // true true
console.log(typeof b1.b3==='function') // true
console.log(typeof b1.b2[2]==='function')
b1.b2[2](4) //调用函数演示
console.log(b1.b3()()) //调用函数演示
1.14
undefined与null
区别:
- undefined代表定义未赋值
- null定义并赋值了, 只是值为null
什么时候给变量赋值为null:
- 初始赋值, 表明将要赋值为对象
- 结束前, 让对象成为垃圾对象(被垃圾回收器回收)
内存、数据、变量
内存:
- 内存条通电后产生的可储存数据的空间
- 产生和死亡: 内存条(电路板)->通电->产生内存空间->存储数据->处理数据->断电->内存空间和数据都消失
- 存放的数据:内部存储的数据、地址值
- 分类:栈: 全局变量/局部变量;堆: 对象
变量:
- 可变化的量, 由变量名和变量值组成
- 每个变量都对应的一块小内存, 变量名用来查找对应的内存, 变量值就是内存中保存的数据
内存,数据, 变量三者之间的关系:
- 内存用来存储数据的空间
- 变量是内存的标识
JS引擎管理内存的方式
内存生命周期
- 分配小内存空间, 得到它的使用权
- 存储数据, 可以反复进行操作
- 释放小内存空间
释放内存
- 局部变量: 函数执行完自动释放
- 对象: 赋值null成为垃圾对象->垃圾回收器回收
对象
必须使用[‘属性名’]的情况
- 属性名包含特殊字符: - 空格
- 属性名不确定
//情况一
p['content-type'] = 'text/json'
console.log(p['content-type'])
//情况二
var propName = 'myAge'
var value = 18
p[propName] = value
console.log(p[propName])
对象字面量和构造函数
var obj = {} 等价于 var obj = new Object(),
所以用对象字面量创建的对象,是Object函数的实例,
函数
call和apply在JS基础上已经记过笔记
回调函数
- 回调函数满足:
- 自己定义
- 没有立即调用
- 最终它在某个时刻或某个条件下执行了
- 常见的回调函数
- dom事件回调函数 ->发生事件的dom元素
- 定时器回调函数 ->window
- ajax请求回调函数(后面讲)
- 生命周期回调函数(后面讲)
原型
博客/自我总结
可以参考以下三篇博客:
JavaScript中Function和Object的原型和原型链
JS中 Object.prototype 原型和原型链 详解
彻底理解什么是原型链,prototype和__proto__的区别(最清晰)
自己总结一下:
- 在函数对象中存在原型对象prototype,在普通对象中没有prototype,但存在__proto__,所有对象都有__proto__属性
- function定义的对象有prototype属性,使用new生成的对象没有prototype属性,存在__proto__
- 特殊的是, Function.prototype没有prototype属性,在控制台输出undefined
- 如果认为普通对象A是对象B的实例,A的__proto__属性等价于B的prototype属性
- Object.__proto__指向Function.prototype(HBuilder的控制台输出结果为function Empty() {}),但是Object.prototype.__proto__指向null
prototype是函数对象自带的一个属性,比如console.log(Object.prototype),控制台打印[object Object];console.log(Function.prototype),控制台打印function Empty() {}。给人的感觉是,打印 自己的“本质”。prototype后又可以继续访问它的__proto__属性,如console.log(Object.prototype.proto),控制台打印null。
而__proto__是普通对象自带的一个属性,普通对象.__proto__即为函数对象.prototype。__proto__给人的感觉是,要追溯它的下一级,比如用构造函数得到的对象,它的__proto__就是new后边的函数,又比如Object其实是一个函数对象,那么console.log(Object.proto)会在控制台打印function Empty() {},即它可等价为Function.prototype。
constructor
每个函数都有一个prototype属性, 它默认指向一个Object空对象(即称为: 原型对象)
原型对象中有一个属性constructor, 它指向函数对象
console.log(Date.prototype.constructor===Date) //true
console.log(Date.prototype.constructor) //function Date() { [native code] }
关于construcor,可以看这篇博客:
javascript 对象中的 constructor属性的作用
显式原型与隐式原型
- 每个函数function都有一个prototype,即显式原型(属性)
- 每个实例对象都有一个__proto__,可称为隐式原型(属性)
- 对象的隐式原型的值为其对应构造函数的显式原型的值
- 内存结构(图)
- 总结:
- 函数的prototype属性: 在定义函数时自动添加的, 默认值是一个空Object对象(该“空”指的是没有程序员指定的属性或方法,不是什么都没有)
- 对象的__proto__属性: 创建对象时自动添加的, 默认值为构造函数的prototype属性值
- 程序员能直接操作显式原型, 但不能直接操作隐式原型(ES6之前)
内存结构图:
原型链
执行上下文和作用域
执行上下文
代码分类(位置)
- 全局代码
- 函数(局部)代码
全局执行上下文
- 在执行全局代码前将window确定为全局执行上下文
- 对全局数据进行预处理
- var定义的全局变量->undefined, 添加为window的属性
- function声明的全局函数->赋值(fun), 添加为window的方法
- this->赋值(window)
- 开始执行全局代码
函数执行上下文
- 在调用函数, 准备执行函数体之前, 创建对应的函数执行上下文对象(虚拟的, 存在于栈中)
- 对局部数据进行预处理
- 形参变量->赋值(实参)->添加为执行上下文的属性
- arguments->赋值(实参列表), 添加为执行上下文的属性
- var定义的局部变量->undefined, 添加为执行上下文的属性
- function声明的函数 ->赋值(fun), 添加为执行上下文的方法
- this->赋值(调用函数的对象)
- 开始执行函数体代码
执行上下文对象个数=调用函数的次数+1(多出的1是window,即全局执行上下文)
<script type="text/javascript">
var a = 10;
var bar = function (x) {
var b = 5;
foo(x + b);
};
var foo = function (y) {
var c = 5;
console.log(a + c + y);
}
bar(10);//调用bar算一次,bar中还要调用foo再算一次,共2次
bar(10);//同理,共2次
</script>
这一段代码中共4+1=5个执行上下文对象
执行上下文栈
- 在全局代码执行前, JS引擎就会创建一个栈来存储管理所有的执行上下文对象
- 在全局执行上下文(window)确定后, 将其添加到栈中(压栈)
- 在函数执行上下文创建后, 将其添加到栈中(压栈)
- 在当前函数执行完后,将栈顶的对象移除(出栈)
- 当所有的代码执行完后, 栈中只剩下window
- 依次输出什么?
gb: undefined
fb: 1
fb: 2
fb: 3
fe: 3
fe: 2
fe: 1
ge: 1- 整个过程中产生了几个执行上下文? 5
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);
作用域链
理解
- 多个上下级关系的作用域形成的链, 它的方向是从下向上的(从内到外)
- 查找变量时就是沿着作用域链来查找的
查找一个变量的查找规则
- 在当前作用域下的执行上下文中查找对应的属性, 如果有直接返回, 否则进入2
- 在上一级作用域的执行上下文中查找对应的属性, 如果有直接返回, 否则进入3
- 再次执行2的相同操作, 直到全局作用域, 如果还找不到就抛出找不到的异常
区别与联系
区别1
- 全局作用域之外,每个函数都会创建自己的作用域,作用域在函数定义时就已经确定了,而不是在函数调用时
- 全局执行上下文环境是在全局作用域确定之后, js代码马上执行之前创建
- 函数执行上下文是在调用函数时, 函数体代码执行之前创建
区别2
- 作用域是静态的, 只要函数定义好了就一直存在, 且不会再变化
- 执行上下文是动态的, 调用函数时创建, 函数调用结束时就会自动释放
联系
- 执行上下文(对象)是从属于所在的作用域
- 全局上下文环境->全局作用域
- 函数上下文环境->对应的函数使用域
1.15
闭包
定义
- 当一个嵌套的内部(子)函数引用了嵌套的外部(父)函数的变量(函数)时, 就产生了闭包
- 闭包是嵌套的内部函数,是包含被引用变量(函数)的对象
- 闭包存在于嵌套的内部函数中,执行函数定义就会产生闭包(不用调用内部函数)
- 产生闭包的条件:
- 函数嵌套
- 内部函数引用了外部函数的数据(变量/函数)
常见闭包
将函数作为另一个函数的返回值
function fn1() {
var a = 2;
function fn2() {
a++;
console.log(a);
}
return fn2;
}
var f = fn1();
f(); // 3
f(); // 4
将函数作为实参传递给另一个函数调用
function showDelay(msg, time) {
setTimeout(function () {
alert(msg);
}, time);
}
showDelay('atguigu', 2000);
闭包的作用
- 使用函数内部的变量在函数执行完后, 仍然存活在内存中,延长了局部变量的生命周期(类比static)
- 让函数外部可以操作(读写)到函数内部的数据(变量/函数)
问题:
- 函数执行完后, 函数内部声明的局部变量一般不存在, 存在于闭包中的变量才可能存在
- 在函数外部不能直接访问函数内部的局部变量吗, 但可以通过闭包让外部操作它
闭包的生命周期
产生: 在嵌套内部函数定义执行完时产生(仅是完成了定义,不是调用)
死亡: 在嵌套的内部函数成为垃圾对象时
function fn1() {
//此时闭包已经产生(函数提升, 内部函数对象已经创建了)
var a = 2;
function fn2 () {
a++;
console.log(a);
}
return fn2;
}
var f = fn1();
f() // 3;
f() // 4;
f = null; //闭包死亡(包含闭包的函数对象成为垃圾对象)
闭包的应用
定义JS模块
- 具有特定功能的js文件
- 将所有的数据和功能都封装在一个函数内部
- 只向外暴露一个包含n个方法的对象或函数
- 模块的使用者,只需要通过模块暴露的对象调用方法来实现对应的功能
1.16
对象创建模式
工厂模式
(这部分在JS基础中讲过,此处是为了和下方的自定义构造函数放在一起)
- 套路: 通过工厂函数动态创建对象并返回
- 适用场景: 需要创建多个对象
- 问题: 对象没有一个具体的类型,都是Object类型
function createPerson(name, age) { //返回一个对象的函数===>工厂函数
var obj = {
name: name,
age: age,
setName: function (name) {
this.name = name;
}
}
return obj;
}
// 创建2个人
var p1 = createPerson('Tom', 12);
var p2 = createPerson('Bob', 13);
// p1和p2是Object类型
自定义构造函数模式
- 套路: 自定义构造函数, 通过new创建对象
- 适用场景: 需要创建多个类型确定的对象
- 问题: 每个对象都有相同的数据,浪费内存
function Person(name, age) {
this.name = name;
this.age = age;
this.setName = function (name) {
this.name = name;
}
}
var p1 = new Person('Tom', 12);
p1.setName('Jack');
console.log(p1.name, p1.age);
console.log(p1 instanceof Person);
//p1确定的类型是Person
function Student (name, price) {
this.name = name;
this.price = price;
}
var s = new Student('Bob', 13000);
console.log(s instanceof Student);
//p2确定的类型是Student
var p2 = new Person('JACK', 23);
console.log(p1, p2);
构造函数+原型 的组合模式
- 套路: 自定义构造函数,属性在函数中初始化, 方法添加到原型上
- 适用场景: 需要创建多个类型确定的对象
function Person(name, age) {
//属性在函数中初始化
this.name = name;
this.age = age;
}
Person.prototype.setName = function (name) {
//向原型中添加方法
this.name = name;
}
var p1 = new Person('Tom', 23);
var p2 = new Person('Jack', 24);
console.log(p1, p2);
1.17
继承模式
原型链继承
(这一块很重要,笔试/面试可能会考察)
套路
- 定义父类型构造函数
- 给父类型的原型添加方法
- 定义子类型的构造函数
- 创建父类型的对象赋值给子类型的原型
- 将子类型原型的构造属性设置为子类型(参考:阮一峰的博客)
- 给子类型原型添加方法
- 创建子类型的对象: 可以调用父类型的方法
关键:子类型的原型为父类型的一个实例对象
//1.定义父类型构造函数
function Supper() {
this.supProp = 'Supper property';
}
//2.给父类型的原型添加方法
Supper.prototype.showSupperProp = function () {
console.log(this.supProp);
}
//3. 定义子类型的构造函数
function Sub() {
this.subProp = 'Sub property';
}
//4. 创建父类型的对象赋值给子类型的原型
Sub.prototype = new Supper();
//5. 将子类型原型的构造属性设置为子类型
Sub.prototype.constructor = Sub;
//6. 给子类型原型添加方法
Sub.prototype.showSubProp = function () {
console.log(this.subProp);
}
//7. 创建子类型的对象: 可以调用父类型的方法
var sub = new Sub();
sub.showSupperProp(); //"Supper property"
// sub.toString();
sub.showSubProp(); //"Sub property"
console.log(sub); // Sub
/*
Sub
subProp: "Sub property"
__proto__: Supper
*/
借用构造函数继承
套路:
- 定义父类型构造函数
- 定义子类型构造函数
- 在子类型构造函数中调用父类型构造(call,或直接用this)
关键:在子类型构造函数中通用call()调用父类型构造函数
// 1. 定义父类型构造函数
function Person(name, age) {
this.name = name;
this.age = age;
}
//2. 定义子类型构造函数
function Student(name, age, salary) {
//3.在子类型构造函数中调用父类型构造
Person.call(this, name, age) ;
// 相当于: this.Person(name, age);
this.salary = salary;
}
var s = new Student('Tom', 20, 14000);
console.log(s.name+" "+s.age+" "+s.salary); //Tom 20 14000
组合继承
原型链+借用构造函数的组合继承
- 利用原型链实现对父类型对象的方法继承
- 利用call()借用父类型构建函数初始化相同属性
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.setName = function (name) {
this.name = name;
}
function Student(name, age, price) {
Person.call(this, name, age); //为了得到属性
this.price = price;
}
Student.prototype = new Person(); //为了能使用父类型的方法
Student.prototype.constructor = Student;
//修正constructor属性(这一句可以没有)
Student.prototype.setPrice = function (price) {
this.price = price;
}
//这里可以去掉,然后在var s下边一行加:s.__proto__.setPrice = ...
//或者直接s.setPrice...
var s = new Student('Tom', 24, 15000);
s.setName('Bob');
s.setPrice(16000);
console.log(s.name+' '+s.age+' '+ s.price);
线程机制与事件机制
进程与线程
进程:程序的一次执行,它占有一片独有的内存空间
线程: CPU的基本调度单位, 是程序执行的一个完整流程
进程与线程:
- 一个进程中一般至少有一个运行的线程: 主线程
- 一个进程中也可以同时运行多个线程,,称程序是多线程运行的
- 一个进程内的数据可以供其中的多个线程直接共享
- 多个进程之间的数据是不能直接共享的
单进程浏览器:firefox、老版IE
多进程浏览器:chrome、新版IE
如何查看浏览器是否是多进程运行:任务管理器->进程
浏览器都是多线程运行的
浏览器内核
浏览器内核:支持浏览器运行的最核心的程序
不同浏览器的内核:Chrome, Safari: webkit;firefox: Gecko;IE: Trident;360,搜狗等国内浏览器: Trident + webkit
内核由很多模块组成:
- html,css文档解析模块 : 负责页面文本的解析
- dom/css模块 : 负责dom/css在内存中的相关处理
- 布局和渲染模块 : 负责页面的布局和效果的绘制
- 定时器模块 : 负责定时器的管理
- 网络请求模块 : 负责服务器请求(常规/Ajax)
- 事件响应模块 : 负责事件的管理
定时器引发的思考
定时器并不能保证真正定时执行,一般会延迟一丁点(可以接受), 也有可能延迟很长时间(不能接受)
document.getElementById('btn').onclick = function () {
var start = Date.now();
console.log('启动定时器前...');
setTimeout(function () {
console.log('定时器执行了', Date.now()-start);
}, 200);
console.log('启动定时器后...');
// 做一个长时间的工作,发现定时器执行的时间远远超过200ms
for (var i = 0; i < 1000000000; i++) {
}
}
//修改i之后的时间,先ctrl+s保存,在浏览器中刷新,再点击按钮,才会显示新的运行时间
定时器回调函数在主线程执行,js是单线程的
定时器是如何实现的:事件循环模型
JS是单线程的
如何证明js执行是单线程的?
- setTimeout()的回调函数是在主线程执行的
- 定时器回调函数只有在运行栈中的代码全部执行完后才有可能执行
为什么js要用单线程模式, 而不用多线程模式?
- JavaScript的单线程,与它的用途有关。
- 作为浏览器脚本语言,JavaScript的主要用途是与用户互动,以及操作DOM。这决定了它只能是单线程,否则会带来很复杂的同步问题。
代码的分类:
- 初始化代码
- 回调代码
JS引擎执行代码的基本流程:
- 执行初始化代码: 包含一些特别的代码、回调函数(异步执行)
- 设置定时器
- 绑定事件监听
- 发送ajax请求
- 后面在某个时刻才会执行回调代码
setTimeout(function () {
console.log('timeout 2rd');
alert('Second');
}, 2000)
setTimeout(function () {
console.log('timeout 1st');
alert('First');
}, 1000)
setTimeout(function () {
console.log('timeout() 0');
}, 0)
function fn() {
console.log('fn()');
}
fn();
console.log('alert()之前');
alert('------');
//暂停当前主线程的执行, 同时暂停计时, 点击确定后, 恢复程序执行和计时
console.log('alert()之后');
该程序执行的次序:
- 执行fn函数,在控制台输出fn()
- 控制台输出alert()之前
- 弹出’------’
- 控制台输出alert()之后
- 控制台输出timeout() 0
- 控制台输出timeout 1st 弹出First
- 控制台输出timeout 2nd 弹出Second
事件循环模型
所有代码分类
- 初始化执行代码(同步代码): 包含绑定dom事件监听, 设置定时器, 发送ajax请求的代码
- 回调执行代码(异步代码): 处理回调逻辑
JS引擎执行代码的基本流程:初始化代码->回调代码
模型的2个重要组成部分:
- 事件(定时器/DOM事件/Ajax)管理模块
- 回调队列
模型的运转流程
- 执行初始化代码,将事件回调函数交给对应模块管理
- 当事件发生时, 管理模块会将回调函数及其数据添加到回调列队中
- 只有当初始化代码执行完后(可能要一定时间), 才会遍历读取回调队列中的回调函数执行
function fn1() {
console.log('fn1()')
}
fn1()
document.getElementById('btn').onclick = function () {
console.log('点击了btn')
}
setTimeout(function () {
console.log('定时器执行了')
}, 2000)
function fn2() {
console.log('fn2()')
}
fn2()
控制台依次打印:
fn1()
fn2()
定时器执行了
点击按钮,打印:点击了btn
Web Workers
这一部分暂时先不学习