前端 - JS进阶
基础总结
数据类型
- 基本数据类型:
String
Number
boolean
undefined
null
- 对象 (引用) 类型:
Object
:任意对象Function
:特殊的对象,可以执行Array
:特殊的对象,内部数据有序,数值索引
判断数据类型
typeof
:返回数据类型的字符串表达,可以判断undefined
、数值、字符串、布尔值、function
,但是判断null
、Object
、Array
时,都会返回object
instanceof
:判断对象的具体类型===
:可以判断undefined
和null
主要使用以上3种方法来判断数据类型。
- 基本数据类型比较,主要用到
typeof
和===
:
var a;
console.log(a, typeof a); // undefined "undefined"
console.log(typeof a === undefined, typeof a === 'undefined'); // false true
console.log(undefined === 'undefined'); // false
console.log(a === undefined); // true
a = Math.PI;
console.log(typeof a === 'number'); // true
console.log(typeof a === 'Number'); // false
a = true;
console.log(typeof a === 'boolean'); // true
a = 'hello';
console.log(typeof a === 'string'); // true
a = null;
console.log(typeof a, typeof a === 'null'); // object false
console.log(a === null); // true
- 判断对象的具体类型:
var b = {
arr: [123, console.log, true, undefined, 'hello'],
func: function() {
return 'test';
}
};
console.log(b instanceof Object); // true
console.log(b.arr instanceof Object, b.arr instanceof Array); // true true
console.log(b.func instanceof Object, b.func instanceof Function); // true true
console.log(typeof b.func === 'function'); // true
console.log(typeof b.arr[1] === 'function'); // true
内存
var a = variable;
,变量a
的内存中保存的是:
- 如果
variable
是基本数据类型,保存的就是数据 - 如果
variable
是对象,保存的是对象的地址值 - 如果
variable
是变量,保存的是变量的内存内容,可能是基本数据,也可能是地址
函数传参数,相当于把变量的内容复制给参数。
function test(num) {
num = 10;
}
var num = 20;
console.log(num); // 20
test(num);
console.log(num); // 20
此时,变量person
的内容,即对象的地址被复制给了参数,所以对象的值被修改了。
var person = {
name: 'Lee',
age: 20
};
function changeAge(obj, newAge) {
obj.age = newAge;
}
console.log(person); // {name: "Lee", age: 20}
changeAge(person, 30);
console.log(person); // {name: "Lee", age: 30}
所以JS中,函数的调用是值传递,在传递对象参数时,值的内容是对象的引用。
JS引擎对内存的管理:
- 内存生命周期:
- 分配小内存空间,得到使用权
- 存储数据,可以多次操作
- 释放小内存空间
- 释放内存:
- 局部变量:函数执行完自动释放
- 对象:成为垃圾对象,等待垃圾回收器回收
function fn() {
var a = {};
}
fn(); // a自动释放, a所指向的对象在之后的某个时刻被垃圾回收器回收
// console.log(a); // ReferenceError: Can't find variable: a
分号
JS语句可以不加分号,但是在以下两种情况下会出现问题:
- 小括号开头的前一条语句
- 中括号开头的前一条语句
可以在行首加分号来解决该问题:
var a = 3
;(function() {
console.log('func');
})()
var b = '123'
;[1, 'hello', true, 5].forEach(function(item, index){
console.log(item, index)
})
对象
访问对象内部数据的方式:
-
对象.属性名,不通用
-
对象['属性名'],通用
obj = {
name: 'object',
123: 456
};
console.log(obj.name); // object
console.log(obj['123']); // 456
必须采取方式二的情况:
- 属性名不符合变量命名规范
- 属性名不确定
var attr = '123';
console.log(obj[attr]); // 456
函数
函数调用
调用函数的方法:
- 直接调用:
test();
- 通过对象调用:
obj.test();
- 使用
new
调用:new test();
- 临时让函数成为对象的方法进行调用:
test.apply(obj);
或test.call(obj);
// obj.test(); // 报错, 因为obj没有test方法
test.apply(obj);
console.log(obj.hello); // Hello!
// obj.test(); // 报错, 因为apply只是临时调用
apply()
只接收一个数组作为参数,而call()
可以接收多个参数
this
this
是什么:
- 所有函数内部都有一个变量
this
- 它的值是调用函数的当前对象
- 任何函数本质上都是通过某个对象调用的,如果没有指定则为
window
对象
确定this
的值:
test()
:window
obj.test()
:obj
new test()
: 新创建的对象test.call(obj, '123')
:obj
函数进阶
原型与原型链
原型prototype
- 每个函数都有一个
prototype
属性,它默认指向一个Object
空对象 - 给
prototype
添加属性 (一般是方法),可以让函数所有实例对象自动拥有原型中的属性
function test() {
console.log('test');
}
console.log(test.prototype);
/* Object{
constructor: ƒ test()
[[Prototype]]: Object
} */
console.log(Date.prototype);
/* Object{
constructor: ƒ Date()
getDate: ƒ getDate()
getDay: ƒ getDay()
...
[[Prototype]]: Object
} */
- 原型对象有一个属性
constructor
,它指向函数对象
console.log(test.prototype.constructor);
/* function test() {
console.log('test');
} */
test.prototype.constructor(); // test
console.log(Date.prototype.constructor === Date); // true
- 向原型对象添加属性:
function Test() {
}
Test.prototype.name = 'test!';
var test = new Test();
console.log(test.name); // test!
显式原型与隐式原型
- 每个函数都有一个
prototype
属性,即显式原型,默认值是一个空Object
的实例对象,Object
除外 - 每个实例对象都有一个
__proto__
属性,即隐式原型,默认值为构造函数的prototype
属性
继续使用上面的例子:
console.log(test.__proto__); // Test {name: "test!"}
console.log(test.__proto__ === Test.prototype); // true
从上面的输出结果可以发现:
- 对象的隐式原型的值即为其对应的构造函数的显示原型的值,它们保存了原型对象的地址值,即指向同一个原型对象
因此,给原型对象添加属性,可以让函数所有实例对象自动拥有原型中的属性。
原型链
在访问一个对象的属性时:
- 首先在自身属性中查找,找到返回
- 如果没有,在
__proto__
中查找,找到返回 - 如果没有,则返回
undefined
function Fn() {
this.test1 = function() {
console.log('test1()');
}
}
Fn.prototype.test2 = function() {
console.log('test2()');
};
func = new Fn();
func.test1(); // test1()
func.test2(); // test2()
console.log(func.test3); // undefined
上面的对象拥有的toString()
等方法,是从Object.prototype
中获取的:
console.log(func.toString()); // [object Object]
console.log(Fn.prototype.__proto__ === Object.prototype); // true
console.log(func.toString === Object.prototype.toString); // true
原型链的尽头是:Object.prototype.__proto__
console.log(Object.prototype.__proto__); // null
于是再看一下上面的这句话:
每个函数都有一个
prototype
属性,即显式原型,默认值是一个空Object
的实例对象,Object
除外
console.log(Fn.prototype instanceof Object); // true
console.log(Object.prototype instanceof Object); // false
- 所有函数都是
Function
的实例,包括Function
自身:
console.log(Function.__proto__ === Function.prototype); // true
console.log(Fn.__proto__ === Function.prototype); // true
对象的属性
- 读取对象的属性值时,自动到原型链中查找
- 设置对象的属性值时,不会查找原型链,而是直接在当前对象中设置属性
- 方法一般定义在原型中,属性一般通过构造函数定义在对象本身上
instanceof
- 对于
a instanceof A
而言:如果A
函数的显式对象A.prototype
在a
对象的原型链上,即a.__proto__
,a.__proto__.__proto__
等,则返回true
,否则返回false
。
function Fn() {
}
var func = new Fn();
console.log(func instanceof Fn); // true
console.log(func instanceof Object); // true
console.log(func.__proto__ === Fn.prototype); // true
console.log(func.__proto__.__proto__ === Object.prototype); // true
Function
是通过new
自己产生的实例
function Fn() {
}
console.log(Fn instanceof Function); // true
console.log(Fn.__proto__ === Function.prototype); // true
console.log(Fn instanceof Object); // true
console.log(Fn.__proto__.__proto__ === Object.prototype); // true
Function
和Object
的关系:
console.log(Object instanceof Function); // true
console.log(Object instanceof Object); // true
console.log(Function instanceof Function); // true
console.log(Function instanceof Object); // true
console.log(Function.prototype === Function.__proto__); // true
console.log(Object.prototype === Function.__proto__.__proto__); // true
console.log(Object.__proto__ === Function.prototype); // true
执行上下文与执行上下文栈
变量声明提升与函数声明提升
- 变量声明提升:使用
var
声明的变量,在定义语句之前就可以访问到,值为undefined
function func() {
console.log(a);
var a = 1;
}
func(); // undefined
上面的函数等价于:
function func() {
var a;
console.log(a);
a = 1;
}
- 函数声明提升:使用
function
声明的函数,在定义之前就可以直接调用
执行上下文
代码分为:全局代码、函数 (局部) 代码
- 全局执行上下文
- 在执行代码之前将
window
确定为全局执行上下文 - 对全局数据进行预处理:
var
定义的全局变量添加为window
的属性,值为undefined
;function
声明的全局函数添加为window
的方法 this
赋值为window
- 在执行代码之前将
- 函数执行上下文
- 调用函数,准备执行函数之前,创建对应函数的执行上下文对象 (虚拟对象,存在于栈中)
- 对局部数据进行预处理:形参变量赋值,添加为执行上下文的属性;
arguments
赋值,添加为执行上下文的属性;var
定义的局部变量,赋值为undefined
,添加为执行上下文的属性;function
声明的函数,添加为执行上下文的方法 this
赋值为调用函数的对象- 开始执行函数代码
function f1(a1) {
console.log(a1); // 123
console.log(a2); // undefined
f2(); // f2!
console.log(this); // Window {Infinity: Infinity, window: Window …}
console.log(arguments); // Arguments [123, 456]
var a2 = 111;
function f2() {
console.log('f2!');
}
}
f1(123, 456);
执行上下文栈
- 全局代码执行前,JS引擎在内存中创建一个栈来存储管理所有的执行上下文对象
- 在全局执行上下文确定后,将其压入栈中
- 在函数执行上下文创建后,将其压入栈中
- 当前函数执行完,将栈顶的上下文对象弹出
- 所有代码执行完之后,栈中只剩下
window
作用域与作用域链
作用域就是一个代码段所在的区域,它是静态的 (相对于上下文对象) ,在编写代码时就确定了。分类:
- 全局作用域
- 函数作用域
- 块作用域 (ES6)
作用域用来隔离变量, 不同作用域下的同名变量不会冲突。
var a = 10;
b = 20;
function f1(x) {
var a = 100, c = 300;
console.log('f1:', a, b, c, x);
function f2(x) {
var a = 1000, d = 400;
console.log('f2:', a, b, c, d, x);
}
f2(100);
f2(200);
}
f1(10);
// f1: 100 20 300 10
// f2: 1000 20 300 400 100
// f2: 1000 20 300 400 200
作用域与执行上下文:
- 全局作用域之外,每个函数都有自己的作用域,作用域在函数定义时已经确定,而不是在函数调用时;全局执行上下文环境实在全局作用域确定之后,JS代码执行前创建;函数执行上下文是在调用函数时,函数代码执行前创建
- 作用域是静态的,只要函数定义好了就一直存在,且不会变化;执行上下文是动态的,调用函数时创建,函数调用结束自动释放
- 上下文环境对象从属于所在的作用域
闭包
循环遍历给按钮加监听:
var btns = document.getElementsByTagName('button');
var len = btns.length;
for(var i = 0; i < len; i++) {
btns[i].index = i;
btns[i].onclick = function() {
console.log(this.index + 1);
};
}
使用闭包也能实现这样的效果:
for (var i = 0; i < len; i++) {
(function (i) {
var btn = btns[i];
btn.onclick = function () {
console.log(i + 1);
}
})(i);
}
闭包的理解
如何产生闭包:
- 当一个嵌套的内部 (子) 函数引用了嵌套的外部 (父) 函数的变量时,就产生了闭包。
产生闭包的条件:
- 函数嵌套
- 内部函数引用了外部函数的数据 (变量/函数)
闭包是什么:
- 闭包存在于嵌套的内部函数中
- 闭包是包含被引用变量/函数的对象
例如:
function f1() {
var a = 123;
var b = 456;
function f2() {
console.log(a);
}
f2(); // 需要运行内部函数
}
f1();
当断点在var b = 456;
时,
[[Scopes]]: Scopes[2]
0: Closure (f1)
a: 123
1: Global {window: Window, self: Window, ...}
当断点在console.log(a);
时,
闭包 (f1)
a: 123
常见的闭包
- 将函数作为嵌套的外部函数的返回值
- 将函数作为实参传递给另一个函数调用
情况1:
function f1() {
var a = 1;
function f2() {
a++;
console.log(a);
}
return f2;
}
var test1 = f1(); // 产生一个闭包
test1(); // 2
test1(); // 3
test1 = null; // 闭包死亡 (包含闭包的函数称为垃圾对象)
var test2 = f1(); // 又产生一个闭包
test2(); // 2
test2(); // 3
函数f1()
中的var a
本来是函数作用域,当函数被调用之后,应当被垃圾回收器回收,但是由于有闭包,它一直存在,并且每次调用test1()
都能自增。
情况2:
function show(msg, delay) {
setTimeout(function() {
console.log(msg);
}, delay);
}
show('Hello', 1000); // Hello
闭包的作用
- 使函数内部的变量在函数执行完之后,仍然存活在函数中,延长局部变量的生命周期
- 让函数外部可以读写函数内部的数据
函数执行完毕之后,一般来说,函数内部声明的局部变量被释放,除非变量存在于闭包中。另外,在函数外部无法访问函数内部的局部变量,但是可以通过闭包来操作。
闭包的应用:自定义JS模块
通过闭包实现模块,好处有:
- 将所有数据和功能都封装在一个函数内部
- 只向外暴露一个包含若干个方法的对象
- 模块的使用者只需要调用对象的方法即能实现相应的功能
/* 1.js */
function myMath() {
var pi = 3.14; // 私有数据
function area(radius) {
return pi * radius * radius;
}
// 向外部暴露的方法
return {
area: area
};
}
/* 2.js */
var module = myMath();
console.log(module.area(10)); // 314
此时:
闭包 (myMath)
pi: 3.14
闭包的缺点
- 函数执行完毕,函数内的布局变量没有释放,占用内存时间较长
- 容易造成内存泄漏
- 内存溢出:程序需要的内存超过了需要的内存
- 内存泄漏:占用的内存没有及时释放,当内存泄漏多了,就容易产生内存溢出
- 常见的内存泄漏:
- 意外的全局变量
- 没有及时清理的计时器或回调函数
- 闭包
所以在使用闭包时,注意及时手动释放内存。
面向对象进阶
对象创建模式
Object构造函数模式
- 先创建空的
Object
对象 - 动态添加属性方法
- 使用场景:创建对象时不确定对象内部数据
- 问题:语句太多
let p = new Object();
p.name = 'Lee';
p['age'] = 19;
p.setAge = function(age) {
p.age = age;
}
console.log(p); // {name: "Lee", age: 19, setAge: function}
p.setAge(30);
console.log(p); // {name: "Lee", age: 30, setAge: function}
对象字面量模式
- 使用
{}
创建对象,同时指定属性和方法 - 使用场景:创建对象时,对象数据是确定的
- 问题:创建多个对象时有重复代码
let p = {
name: 'Lee',
age: 20,
setAge: function(age) {
this.age = age;
}
};
console.log(p); // {name: "Lee", age: 20, setAge: function}
p.setAge(30);
console.log(p); // {name: "Lee", age: 30, setAge: function}
工厂模式
- 通过工厂函数动态创建对象并返回
- 使用场景:需要创建多个对象
- 问题:对象没有具体类型,都是
Object
类型
function createPerson(name, age) {
var p = {
name: name,
age: age,
setAge: function (age) {
this.age = age;
}
};
return p;
}
p = createPerson('Lee', 20);
console.log(p); // {name: "Lee", age: 20, setAge: function}
p.setAge(30);
console.log(p); // {name: "Lee", age: 30, setAge: function}
自定义构造函数模式
- 自定义构造函数,通过
new
创建对象 - 使用场景:需要创建多个同一类型的对象
- 问题:每个对象都有相同的数据 (方法),浪费内存,需要放到原型对象中
function Person(name, age) {
this.name = name;
this.age = age;
this.setAge = function (age) {
this.age = age;
};
}
p = new Person('Lee', 20);
console.log(p); // Person {name: "Lee", age: 20, setAge: function}
p.setAge(30);
console.log(p); // Person {name: "Lee", age: 30, setAge: function}
构造函数+原型模式
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.setAge = function (age) {
this.age = age;
};
p = new Person('Lee', 20);
console.log(p); // Person {name: "Lee", age: 20, setAge: function}
p.setAge(30);
console.log(p); // Person {name: "Lee", age: 30, setAge: function}
继承模式
原型链继承
原理:子类型的原型为父类型的一个实例对象
- 定义父类型构造方法
- 给父类型的原型添加方法
- 定义子类型构造函数
- 创建父类型对象,并赋值给子类型的原型
- 将子类型原型的构造属性 (
constructor
) 设置为子类型 - 给子类型原型添加方法
- 创建子类型对象:继承父类型的方法
// 1. 定义父类型构造方法
function Super() {
this.superProp = 'SuperProp!';
}
// 2. 给父类型的原型添加方法
Super.prototype.showSuperProp = function() {
console.log(this.superProp);
}
// 3. 定义子类型构造函数
function Sub() {
this.subProp = 'SubProp!';
}
// 4. 创建父类型对象,并赋值给子类型的原型
Sub.prototype = new Super();
// 5. 将子类型原型的构造属性 (constructor) 设置为子类型
Sub.prototype.constructor = Sub;
// 6. 给子类型原型添加方法
Sub.prototype.showSubProp = function() {
console.log(this.subProp);
}
var sub = new Sub();
sub.showSubProp(); // SubProp!
sub.showSuperProp(); // SuperProp!
console.log(sub.toString()); // [object Object]
借用构造函数继承
- 定义父类型构造函数
- 定义子类型构造函数
- 在子类型构造函数中调用父类型构造函数
function Person(name, age) {
this.name = name;
this.age = age;
}
function Student(name, age, grade) {
Person.call(this, name, age);
this.grade = grade;
}
组合继承
结合上面两种方法:
- 利用原型链继承父类的方法
- 利用父类型构造函数,为子类型初始化相同的属性
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.sayName = function() {
console.log('I am ' + this.name);
};
function Student(name, age, grade) {
Person.call(this, name, age);
this.grade = grade;
}
Student.prototype = new Person();
Student.prototype.constructor = Student;
Student.prototype.sayGrade = function() {
console.log('Grade: ' + this.grade);
};
var s = new Student('Lee', 20, 9);
console.log(s.name, s.age, s.grade); // Lee 20 9
s.sayName(); // I am Lee
s.sayGrade(); // Grade: 9
线程机制与事件机制
进程与线程
- 进程process:程序的一次执行,它占有一片独有的内存空间
- 线程thread:进程内的一个独立执行单元,程序执行的一个完整流程,CPU调度的最小单元
- 线程池tread pool:保存多个线程对象的容器,实现线程对象的反复利用
应用程序必须运行在某个进程的某个线程上。一个进程中至少有一个运行的线程:主线程,进程启动之后自动创建。进程内的数据可以由其中的多个线程直接共享,进程之间的数据不能直接共享。
多进程与多线程:
- 多进程:一个应用程序可以同时启动多个实例;多线程:一个进程中,同时有多个线程运行;
多线程与单线程:
- 多线程的优点是能有效提升CPU利用率,缺点是创建多线程的开销、线程切换的开销、死锁和状态同步的问题;单线程的优点是顺序编程简单,缺点是效率低、
JS:
- JS是单线程运行的
- 使用H5的Web Workers可以多线程运行
浏览器:
- 多线程运行
浏览器内核
浏览器内核是支撑浏览器运行的最核心程序:
- Chrome、Safari:webkit
- firefox:Gecko
浏览器内核由许多模块组成:
- 主线程:
- JS引擎模块:负责JS程序的编译与运行
- HTML、CSS文档解析模块:负责页面文本的解析
- DOM/CSS模块:负责DOM和CSS在内存中的处理
- 布局和渲染模块:负责页面的布局和效果的绘制
- 分线程:
- 定时器模块:负责定时器的管理
- 事件响应模块:负责事件的管理
- 网络请求模块:负责ajax请求
定时器
- 定时器不能保证真正的定时执行,一般会延迟一点时间
- 定时器的回调函数在主线程中执行
- 定时器的实现:事件循环模型