数据类型
1.分类:基本类型(保存基本类型数据的变量)和引用类型(保存对象地址值的变量)
- 基本类型:Number: 任意数值
String: 任意文本
Boolean: true/false
undefined: undefined
null: null - 引用类型:object、Array、Function
2.判断
- typeof(typeof xxx)
可以区别: 数值, 字符串, 布尔值, undefined, function
不能区别: null与对象, 一般对象与数组 - instanceof(xxx instanceof Object / Array / Function)
专门用来判断对象数据的类型: Object, Array与Function
数据、变量、内存
1.内存包含2个数据:
内部存储的数据(一般数据/地址数据)
内存地址值数据
2. 内存分类:
栈: 全局变量, 局部变量 (空间较小)
堆: 对象 (空间较大)
只有当定义一个对象时才会保存地址值,其他都是直接保存数据
3.内存,数据, 变量三者之间的关系
内存是一个容器, 用来存储程序运行需要操作的数据
变量是内存的标识, 我们通过变量找到对应的内存, 进而操作(读/写)内存中的数据
例如
var obj = { name: "tom" };
var a = obj;
a = "jerry";
console.log(a, obj);
此时a的保存内容不是地址值,更改成了jerry,而obj不作改变。
a.name="jerry"
输出:a里面保存的是地址值,根据这个地址值去寻找,找到name:“Tom”,且将name的值修改了,此时obj也会变化
关于引用变量赋值问题
- 2个引用变量指向同一个对象, 通过一个引用变量修改对象内部数据, 另一个引用变量也看得见
- 2个引用变量指向同一个对象,让一个引用变量指向另一个对象, 另一个引用变量还是指向原来的对象
var obj1 = { name: "tom" };
var obj2 = obj1;
obj1.name = "jack";
console.log(obj2.name); //jack
//1. 2个引用变量指向同一个对象(保存同一个地址值), 通过一个引用变量修改对象内部数据, 另一个引用变量也看得见
var obj1 = {};
var obj2 = obj1;
obj2.name = "Tom";
console.log(obj1.name);//Tom
function f1(obj) {
obj.age = 12;
}
f1(obj2);
console.log(obj1.age);//12
//2. 2个引用变量指向同一个对象,让一个引用变量指向另一个对象, 另一个引用变量还是指向原来的对象
var obj3 = { name: "Tom" };
var obj4 = obj3;
obj3 = { name: "JACK" }; //kack
console.log(obj4.name);//tom
function f2(obj) {
obj = { name: "Bob" };//垃圾回收
//例如:obj=obj4,obj={ name: "Bob" },obj不再指向obj4的地址值,所以不会改变obj4.name的值
}
f2(obj4);
console.log(obj4.name);//tom
JS引擎如何管理内存
问题: JS引擎如何管理内存?
- 内存生命周期
1). 分配小内存空间,得到它的使用权
2). 存储数据,可以反复进行操作
3). 释放小内存空间 - 释放内存
- 为执行函数分配的栈空间内存(局部变量): 函数执行完自动释放
- 存储对象的堆空间内存(对象): 当内存没有引用指向时, 对象成为垃圾对象, 垃圾回收器后面就会回收释放此内存
var a = 3;
var obj = {}; //这两个占用三个空间,a = 3、obj、{}
obj = null; // 此时占用两个空间,a = 3、obj,{}已经释放
function fn() {
var b = {};
}
fn(); // 局部变量是函数执行时创建,执行完毕后函数内部所有的局部变量自动释放。b是自动释放,b所指向的对象是在后面的某个时刻由垃圾回收器回收
对象(属性名不确定、有特殊符号)
/*情形一: 属性名不是合法的标识名*/
/*需求: 添加一个属性: content-type: text/json */
// p.content-type = 'text/json' //不正确
p['content-type'] = 'text/json'
/*情形二: 属性名不确定*/
var prop = 'xxx'
var value = 123
// p.prop = value //不正确
p[prop] = value
console.log(p['content-type'], p[prop])
IIEF(立即执行函数、匿名函数自调用)
作用
- 隐藏内部实现
- 不污染外部命名空间
- 用它来编写js模块
(function (i) {
var a = 4;
function fn() {
console.log("fn ", i + a);
}
fn();
})(3);
this
对象不构成单独的作用域
1.如果jumps是箭头函数,因为对象不构成单独的作用域,导致jumps箭头函数定义时的作用域就是全局作用域。
如果jumps是普通函数,该方法内部的this指向cat
const cat = {
lives: 9,
jumps: () => {
this.lives--;//报错,this不指向cat,指向全局
}
jumps: function(){
this.lives--;//不报错,this指向cat,普通函数的this,谁调用就指向谁
}
}
- this是什么?
- 一个关键字, 一个内置的引用变量
- 在函数中都可以直接使用this
- this代表调用函数的当前对象
- 在定义函数时, this还没有确定, 只有在执行时才动态确定(绑定)的
- 如何确定this的值?
- test()
- obj.test()
- new test()
- test.call(obj)
前置知识: - 本质上任何函数在执行时都是通过某个对象调用的
function Person(color) {
console.log(this); //windows
this.color = color;
this.getColor = function () {
console.log(this); //windows
return this.color;
};
this.setColor = function (color) {
console.log(this); //windows
this.color = color;
};
}
Person("red"); //this是谁? windows
var p = new Person("yello"); //this是谁? p
p.getColor(); //this是谁? p
var obj = {};
p.setColor.call(obj, "black"); //this是谁? obj
var test = p.setColor;
test(); //this是谁? windows
function fun1() {
console.log(this); //windows
function fun2() {
console.log(this);
}
fun2(); //this是谁?windows
}
fun1(); //windows;
是否加分号问题
可加可不加。
在下面2种情况下不加分号会有问题
- 小括号开头的前一条语句
- 中方括号开头的前一条语句
- 解决办法: 在行首加分号
// 情形一: 小括号开头的前一条语句
var a = 3
;(function () {
})
/*
错误理解: 将3看成是函数调用
var a = 3(function () {
})
*/
// 情形二: 中方括号开头的前一条语句
var b = a
;[1, 3, 5].forEach(function (item) {
console.log(item)
})
/*
错误理解:
a = b[5].forEach(function(e){
console.log(e)
})
函数原型与原型链
1.函数的prototype属性:
- 每个函数都有一个prototype属性,它默认指向一个object空对象(即称为:原型对象)
- 原型对象上有一个属性constructor,它指向函数对象
2.给原型对象添加属性(一般都是方法) - 作用:函数的所有实例对象自动拥有原型中的属性和方法
原型上面的方法是给实例对象用的
例题:
显示与隐式原型
- 每个函数function都有一个prototype,即显式原型(属性)
- 每个实例对象都有一个__proto__,可称为隐式原型
- 对象的隐式原型的值为其对应构造函数的显式原型的值(fn.proto === Fn.prototype)
- 内存结构(图)
- 总结:
- 函数的prototype属性: 在定义函数时自动添加的, 默认值是一个空Object对象
- 对象的__proto__属性: 创建对象时自动添加的, 默认值为构造函数的prototype属性值
- 程序员能直接操作显式原型, 但不能直接操作隐式原型(ES6之前)
function Fn() {
//创建时:内部执行一个语句:this.prototype={}
}
var fn = new Fn(); //内部语句:this.__proto__=Fn.prototype
console.log(fn);
var fn = new Fn();
console.log(Fn.prototype === fn.__proto__); //true
Fn.prototype.test = function () {
console.log("test()");
};
//通过实例对象调用原型上的方法
fn.test();
原型链
1.函数的显式原型指向的对象默认式空Object对象(object除外,因为它的__proto__为nulll)
2.所有函数都是Function的实例(包括Function)
3.object的原型对象是原型链的尽头
var O1=new Object();
var o2={}
function Foo(){ }
原型链属性
- 读取对象的属性值时: 会自动到原型链中查找
- 设置对象的属性值时: 不会查找原型链, 如果当前对象中没有此属性, 直接添加此属性并设置其值
- 方法一般定义在原型中, 属性一般通过构造函数定义在对象本身上
function Fn() {}
Fn.prototype.a = "xxx";
var fn1 = new Fn();
var fn2 = new Fn();
fn2.a = "ccc"; //设置属性值不会去查找原型链
console.log(fn1.a, fn2.a, fn1, fn2);
function Fn() {}
Fn.prototype.a = "xxx";
var fn1 = new Fn();
var fn2 = new Fn();
fn2.__proto__.a = "yyy"; //操作隐式原型上的属性
console.log(fn1.a, fn2.a, fn1, fn2);
instanceof
变量(函数)提升
- 变量声明提升
- 通过var定义(声明)的变量, 在定义语句之前就可以访问到
- 值: undefined
- 函数声明提升
- 通过function声明的函数, 在之前就可以直接调用
- 值: 函数定义(对象)
var a = 4;
function fn() {
console.log(a); //输出undefined,变量提升之前没有赋值
var a = 5;
}
fn();
/*变量提升*/
console.log(a1); //可以访问, 但值是undefined
/*函数提升*/
a2(); // 可以直接调用
var a1 = 3;
function a2() {
console.log("a2()");
}
实行上下文
- 代码分类(位置)
- 全局代码
- 函数代码
- 全局执行上下文(执行前需要做准备工作)
- 在执行全局代码前将window确定为全局执行上下文
- 对全局数据进行预处理
- var定义的全局变量==>undefined, 添加为window的属性
- function声明的全局函数==>赋值(fun), 添加为window的方法
- this==>赋值(window)
- 开始执行全局代码
- 函数执行上下文
- 在调用函数, 准备执行函数体之前, 创建对应的函数执行上下文对象(虚拟的,存在于栈中)
- 对局部数据进行预处理
- 形参变量==>赋值(实参)==>添加为执行上下文的属性
- arguments==>赋值(实参列表), 添加为执行上下文的属性
- var定义的局部变量==>undefined, 添加为执行上下文的属性
- function声明的函数 ==>赋值(fun), 添加为执行上下文的方法
- this==>赋值(调用函数的对象)
- 开始执行函数体代码
/*
测试题1: 先预处理变量, 后预处理函数
*/
function a() {}
var a;
console.log(typeof a); //'function'
/*
测试题2: 变量预处理, in操作符
*/
if (!(b in window)) {
var b = 1;
}
console.log(b); //undefined
/*
测试题3: 预处理, 顺序执行
*/
var c = 1;
function c(c) {
console.log(c);
var a = 3;
}
c(2); //报错
作用域
函数的作用域是在定义时就确定好的
由于fn函数是在全局中定义的,此时x也是在本身找,找不到去全局,所以为10而不是20
/*
问题: 结果输出多少?
*/
var x = 10;
function fn() {
console.log(x);//输出10
}
function show(f) {
var x = 20;
f();
}
show(fn);
/*
说说它们的输出情况
*/
var fn = function () {
console.log(fn)
}
fn()
var obj = {
fn2: function () {
console.log(fn2)//报错,如果是this.fn2可以找到这个fn2函数
}
}
obj.fn2()
闭包
引入:for循环例题
由于var定义的函数没有块级作用域,而且setTimeout是一个回调函数,在某个时刻才会发送,for循环完毕之后i=3,所以输出的是三个3
for (var i = 0; i < 3; i++) {
setTimeout(() => {
console.log(i); //输出三个i
}, 100);
}
如果改成let定义i,可以输出012
因为let有块级作用域,所以i的值不会受外面的i影响
for (let i = 0; i < 3; i++) {
setTimeout(() => {
console.log(i); //输出0 1 2
}, 100);
}
改成闭包:
for (let i = 0; i < 3; i++) {
(function (i) {//此处的i是局部变量
setTimeout(() => {
console.log(i); //输出0 1 2
}, 100);
})(i);//此处的i是全局变量
}
常见的闭包
- 将函数作为另一个函数的返回值
- 将函数作为实参传递给另一个函数调用
//1.将函数作为另外一个函数的返回值
function fn1() {
var a = 2;
function fn2() {
a++;
console.log(a);
}
return fn2;
}
var f = fn1();
f();
f();
// 1. 将函数作为另一个函数的返回值
function fn1() {
var a = 2;
function fn2() {
a++;
console.log(a);
}
return fn2;
}
var f = fn1();
f(); // 3
f(); // 4
// 2. 将函数作为实参传递给另一个函数调用
function showMsgDelay(msg, time) {
setTimeout(function () {
console.log(msg);
}, time);
}
showMsgDelay("hello", 1000);
闭包的作用
- 使用函数内部的变量在函数执行完后, 仍然存活在内存中(延长了局部变量的生命周期)
- 让函数外部可以操作(读写)到函数内部的数据(变量/函数)
问题:
- 函数执行完后, 函数内部声明的局部变量是否还存在? 一般不存在,只有在闭包里的才有可能存在
- 在函数外部能直接访问函数内部的局部变量吗? 不能,但可以通过闭包让外部操作它
function fun1() {
var a = 3;
function fun2() {
a++; //引用外部函数的变量--->产生闭包
console.log(a);
}
return fun2;
}
var f = fun1(); //由于f引用着内部的函数-->内部函数以及闭包都没有成为垃圾对象
f(); //间接操作了函数内部的局部变量
f();
闭包的生命周期
- 产生: 在嵌套内部函数定义执行完时就产生了(不是在调用)
- 死亡: 在嵌套的内部函数成为垃圾对象时
function fun1() {
//此处闭包已经产生,(因为函数提升,内部函数对象已经创建)
var a = 3;
function fun2() {
a++;
console.log(a);
}
return fun2;
}
var f = fun1();
f();
f();
f = null; //闭包对象死亡(包含闭包的函数对象成为垃圾对象)
闭包函数的应用
定义JS模块
- 具有特定功能的js文件
- 将所有的数据和功能都封装在一个函数内部(私有的)
- 只向外暴露一个包含n个方法的对象或函数
- 模块的使用者, 只需要通过模块暴露的对象调用方法来实现对应的功能
<script type="text/javascript" src="05_coolModule.js"></script>
<script type="text/javascript">
var module = coolModule();
module.doSomething();
module.doOtherthing();
</script>
外部05_coolModule.js"文件
/**
* 自定义模块1
*/
function coolModule(msg) {
//私有的数据
//私有的操作数据的函数
function doSomething() {
console.log(msg.toUpperCase());
}
function doOtherthing() {
console.log(msg.toLowerCase());
}
//向外暴露包含多个方法的对象
return {
doSomething: doSomething,
doOtherthing: doOtherthing,
};
}
也可以将该属性定义在全局中,直接使用
/**
* 自定义模块2
*/
(function (window) {
//私有的数据
var msg = 'atguigu'
var names = ['I', 'Love', 'you']
//操作数据的函数
function a() {
console.log(msg.toUpperCase())
}
function b() {
console.log(names.join(' '))
}
window.coolModule2 = {
doSomething: a,
doOtherthing: b
}
})(window)
练习题:
/*
说说它们的输出情况
*/
function fun(n, o) {
console.log(o);
return {
fun: function (m) {
return fun(m, n);
},
};
}
var a = fun(0);
a.fun(1);
a.fun(2);
a.fun(3); //undefined,0,0,0
var b = fun(0).fun(1).fun(2).fun(3); //undefined,0,1,2
var c = fun(0).fun(1);
c.fun(2);
c.fun(3); //undefined,0,1,1
内存溢出与内存泄露
- 内存溢出
- 一种程序运行出现的错误
- 当程序运行需要的内存超过了剩余的内存时, 就出抛出内存溢出的错误
- 内存泄露
- 占用的内存没有及时释放
- 内存泄露积累多了就容易导致内存溢出
- 常见的内存泄露:
- 意外的全局变量
- 没有及时清理的计时器或回调函数
- 闭包
Object构造函数模式
方式一: Object构造函数模式
- 套路: 先创建空Object对象, 再动态添加属性/方法
- 适用场景: 起始时不确定对象内部数据
- 问题: 语句太多
var p = new Object()
p = {}
p.name = 'Tom'
p.age = 12
p.setName = function (name) {
this.name = name
}
p.setaAge = function (age) {
this.age = age
}
对象字面量模式
- 套路: 使用{}创建对象, 同时指定属性/方法
- 适用场景: 起始时对象内部数据是确定的
- 问题: 如果创建多个对象, 有重复代码
var p = {
name: 'Tom',
age: 23,
setName: function (name) {
this.name = name
}
}
console.log(p.name, p.age)
p.setName('JACK')
console.log(p.name, p.age)
var p2 = {
name: 'BOB',
age: 24,
setName: function (name) {
this.name = name
}
}
工厂模式
方式三: 工厂模式
- 套路: 通过工厂函数动态创建对象并返回
- 适用场景: 需要创建多个对象
- 问题: 对象没有一个具体的类型, 都是Object类型
// 工厂函数: 返回一个需要的数据的函数
function createPerson(name, age) {
var p = {
name: name,
age: age,
setName: function (name) {
this.name = name
}
}
return p
}
var p1 = createPerson('Tom', 12)
var p2 = createPerson('JAck', 13)
console.log(p1)
console.log(p2)
自定义构造函数模式
方式四: 自定义构造函数模式
- 套路: 自定义构造函数, 通过new创建对象
- 适用场景: 需要创建多个类型确定的对象
- 问题: 每个对象都有相同的数据, 浪费内存
function Person(name, age) {
this.name = name
this.age = age
this.setName = function (name) {
this.name = name
}
}
var p1 = new Person('Tom', 12)
var p2 = new Person('Tom2', 13)
console.log(p1, p1 instanceof Person)
构造函数+原型的组合模式
方式六: 构造函数+原型的组合模式
- 套路: 自定义构造函数, 属性在函数中初始化, 方法添加到原型上
- 适用场景: 需要创建多个类型确定的对象
function Person (name, age) {
this.name = name
this.age = age
}
Person.prototype.setName = function (name) {
this.name = name
}
var p1 = new Person('Tom', 12)
var p2 = new Person('JAck', 23)
p1.setName('TOM3')
console.log(p1)
Person.prototype.setAge = function (age) {
this.age = age
}
p1.setAge(23)
console.log(p1.age)
Person.prototype = {}
p1.setAge(34)
console.log(p1)
var p3 = new Person('BOB', 12)
p3.setAge(12)
原型链继承
方式1: 原型链继承(为了得到父函数的属性、方法)
- 套路
1. 定义父类型构造函数
2. 给父类型的原型添加方法
3. 定义子类型的构造函数
4. 创建父类型的对象赋值给子类型的原型
5. 将子类型原型的构造属性设置为子类型
6. 给子类型原型添加方法
7. 创建子类型的对象: 可以调用父类型的方法 - 关键
1. 子类型的原型为父类型的一个实例对象
/**
* 6.原型链继承
* 套路:
* 1-定义父类型和子类型的构造函数
* 2-给父类型、子类型原型添加方法(给子类型原型添加方法要3、4定义之后)
* 3-子类型的原型为父类型的一个实例对象
* 4-更正子类型的构造函数为子类型
* 5-创建的子类型的对象:可以调用父类型的方法
*/
function Supper() { //父类型
this.superProp = 'The super prop'
}
//原型的数据所有的实例对象都可见
Supper.prototype.showSupperProp = function () {
console.log(this.superProp)
}
function Sub() { //子类型
this.subProp = 'The sub prop'
}
// 子类的原型为父类的实例
Sub.prototype = new Supper()
// 修正Sub.prototype.constructor为Sub本身
Sub.prototype.constructor = Sub
Sub.prototype.showSubProp = function () {
console.log(this.subProp)
}
// 创建子类型的实例
var sub = new Sub()
// 调用父类型的方法
sub.showSubProp()
// 调用子类型的方法
sub.showSupperProp()
借用构造函数继承(假的)
方式2: 借用构造函数继承(假的)
- 套路:
- 定义父类型构造函数
- 定义子类型构造函数
- 在子类型构造函数中调用父类型构造
- 关键:
- 在子类型构造函数中通用super()调用父类型构造函数
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.name = name; 相当于Person.call(this, name, age);
// this.age = age;
this.price = price;
}
var s = new Student("Tom", 20, 12000);
console.log(s.name, s.age, s.price);
组合继承
方式3: 原型链+借用构造函数的组合继承
- 利用原型链实现对父类型对象的方法继承
- 利用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 = new Student("Tom", 12, 10000);
s.setPrice(11000);
s.setName("Bob");
console.log(s);
console.log(s.constructor);
进程与线程
- 进程:程序的一次执行, 它占有一片独有的内存空间
- 线程: CPU的基本调度单位, 是程序执行的一个完整流程
- 进程与线程
- 一个进程中一般至少有一个运行的线程: 主线程
- 一个进程中也可以同时运行多个线程, 我们会说程序是多线程运行的
- 一个进程内的数据可以供其中的多个线程直接共享
- 多个进程之间的数据是不能直接共享的
- 浏览器运行是单进程还是多进程?
- 有的是单进程
- firefox
- 老版IE
- 有的是多进程
- chrome
- 新版IE
- 如何查看浏览器是否是多进程运行的呢?
- 任务管理器==>进程
- 浏览器运行是单线程还是多线程?
- 都是多线程运行的
浏览器内核
- 什么是浏览器内核?
- 支持浏览器运行的最核心的程序
- 不同的浏览器可能不太一样
- Chrome, Safari: webkit
- firefox: Gecko
- IE: Trident
- 360,搜狗等国内浏览器: Trident + webkit
- 内核由很多模块组成
-
js引擎模板:负责js程序的编译与运行
-
html,css文档解析模块 : 负责页面文本的解析
-
dom/css模块 : 负责dom/css在内存中的相关处理
-
布局和渲染模块 : 负责页面的布局和效果的绘制
-
定时器模块 : 负责定时器的管理
-
网络请求模块 : 负责服务器请求(常规/Ajax)
-
事件响应模块 : 负责事件的管理
js是单线程执行的
- 如何证明js执行是单线程的?
- setTimeout()的回调函数是在主线程执行的
- 定时器回调函数只有在运行栈中的代码全部执行完后才有可能执行
- 为什么js要用单线程模式, 而不用多线程模式?
- JavaScript的单线程,与它的用途有关。
- 作为浏览器脚本语言,JavaScript的主要用途是与用户互动,以及操作DOM。
- 这决定了它只能是单线程,否则会带来很复杂的同步问题
- 代码的分类:
- 初始化代码
- 回调代码
- js引擎执行代码的基本流程
- 先执行初始化代码: 包含一些特别的代码
- 设置定时器
- 绑定监听
- 发送ajax请求
- 后面在某个时刻才会执行回调代码
事件循环模型
- 所有代码分类
- 初始化执行代码(同步代码): 包含绑定dom事件监听, 设置定时器, 发送ajax请求的代码
- 回调执行代码(异步代码): 处理回调逻辑
- js引擎执行代码的基本流程:
- 初始化代码===>回调代码
- 模型的2个重要组成部分:
- 事件(定时器、DOM事件、AJAX)管理模块
- 回调队列
- 模型的运转流程
- 执行初始化代码, 将事件回调函数交给对应模块管理
- 当事件发生时, 管理模块会将回调函数及其数据添加到回调列队中
- 只有当初始化代码执行完后(可能要一定时间), 才会遍历读取回调队列中的回调函数执行