前端 - JS进阶

JS文档

基础总结

数据类型

  • 基本数据类型:
    • String
    • Number
    • boolean
    • undefined
    • null
  • 对象 (引用) 类型:
    • Object:任意对象
    • Function:特殊的对象,可以执行
    • Array:特殊的对象,内部数据有序,数值索引

判断数据类型

  1. typeof:返回数据类型的字符串表达,可以判断undefined、数值、字符串、布尔值、function,但是判断nullObjectArray时,都会返回object
  2. instanceof:判断对象的具体类型
  3. ===:可以判断undefinednull

主要使用以上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引擎对内存的管理:

  • 内存生命周期:
    1. 分配小内存空间,得到使用权
    2. 存储数据,可以多次操作
    3. 释放小内存空间
  • 释放内存:
    1. 局部变量:函数执行完自动释放
    2. 对象:成为垃圾对象,等待垃圾回收器回收
function fn() {
    var a = {};
}
fn();  // a自动释放, a所指向的对象在之后的某个时刻被垃圾回收器回收

// console.log(a);  // ReferenceError: Can't find variable: a

分号

JS语句可以不加分号,但是在以下两种情况下会出现问题:

  1. 小括号开头的前一条语句
  2. 中括号开头的前一条语句

可以在行首加分号来解决该问题:

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

必须采取方式二的情况:

  1. 属性名不符合变量命名规范
  2. 属性名不确定
var attr = '123';
console.log(obj[attr]);  // 456

函数

函数调用

调用函数的方法:

  1. 直接调用:test();
  2. 通过对象调用:obj.test();
  3. 使用new调用:new test();
  4. 临时让函数成为对象的方法进行调用: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是什么:

  1. 所有函数内部都有一个变量this
  2. 它的值是调用函数的当前对象
  3. 任何函数本质上都是通过某个对象调用的,如果没有指定则为window对象

确定this的值:

  1. test(): window
  2. obj.test(): obj
  3. new test(): 新创建的对象
  4. 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!

显式原型与隐式原型

  1. 每个函数都有一个prototype属性,即显式原型,默认值是一个空Object的实例对象,Object除外
  2. 每个实例对象都有一个__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

对象的属性

  1. 读取对象的属性值时,自动到原型链中查找
  2. 设置对象的属性值时,不会查找原型链,而是直接在当前对象中设置属性
  3. 方法一般定义在原型中,属性一般通过构造函数定义在对象本身上

instanceof

  • 对于a instanceof A而言:如果A函数的显式对象A.prototypea对象的原型链上,即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
  • FunctionObject的关系:
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的属性,值为undefinedfunction声明的全局函数添加为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);

执行上下文栈

  1. 全局代码执行前,JS引擎在内存中创建一个栈来存储管理所有的执行上下文对象
  2. 在全局执行上下文确定后,将其压入栈中
  3. 在函数执行上下文创建后,将其压入栈中
  4. 当前函数执行完,将栈顶的上下文对象弹出
  5. 所有代码执行完之后,栈中只剩下window

作用域与作用域链

作用域就是一个代码段所在的区域,它是静态的 (相对于上下文对象) ,在编写代码时就确定了。分类:

  1. 全局作用域
  2. 函数作用域
  3. 块作用域 (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

作用域与执行上下文:

  1. 全局作用域之外,每个函数都有自己的作用域,作用域在函数定义时已经确定,而不是在函数调用时;全局执行上下文环境实在全局作用域确定之后,JS代码执行前创建;函数执行上下文是在调用函数时,函数代码执行前创建
  2. 作用域是静态的,只要函数定义好了就一直存在,且不会变化;执行上下文是动态的,调用函数时创建,函数调用结束自动释放
  3. 上下文环境对象从属于所在的作用域

闭包

循环遍历给按钮加监听:

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);
}

闭包的理解

如何产生闭包:

  • 当一个嵌套的内部 (子) 函数引用了嵌套的外部 (父) 函数的变量时,就产生了闭包。

产生闭包的条件:

  1. 函数嵌套
  2. 内部函数引用了外部函数的数据 (变量/函数)

闭包是什么:

  • 闭包存在于嵌套的内部函数中
  • 闭包是包含被引用变量/函数的对象

例如:

function f1() {
    var a = 123;
    var b = 456;

    function f2() {
        console.log(a);
    }
    f2();  // 需要运行内部函数
}

f1();

当断点在var b = 456;时,

image

[[Scopes]]: Scopes[2]
	0: Closure (f1)
		a: 123
	1: Global {window: Window, self: Window, ...}

当断点在console.log(a);时,

闭包 (f1)
	a: 123

常见的闭包

  1. 将函数作为嵌套的外部函数的返回值
  2. 将函数作为实参传递给另一个函数调用

情况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

闭包的作用

  1. 使函数内部的变量在函数执行完之后,仍然存活在函数中,延长局部变量的生命周期
  2. 让函数外部可以读写函数内部的数据

函数执行完毕之后,一般来说,函数内部声明的局部变量被释放,除非变量存在于闭包中。另外,在函数外部无法访问函数内部的局部变量,但是可以通过闭包来操作。

闭包的应用:自定义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

闭包的缺点

  1. 函数执行完毕,函数内的布局变量没有释放,占用内存时间较长
  2. 容易造成内存泄漏
    • 内存溢出:程序需要的内存超过了需要的内存
    • 内存泄漏:占用的内存没有及时释放,当内存泄漏多了,就容易产生内存溢出
  • 常见的内存泄漏:
    • 意外的全局变量
    • 没有及时清理的计时器或回调函数
    • 闭包

所以在使用闭包时,注意及时手动释放内存。

面向对象进阶

对象创建模式

Object构造函数模式

  1. 先创建空的Object对象
  2. 动态添加属性方法
  3. 使用场景:创建对象时不确定对象内部数据
  4. 问题:语句太多
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}

对象字面量模式

  1. 使用{}创建对象,同时指定属性和方法
  2. 使用场景:创建对象时,对象数据是确定的
  3. 问题:创建多个对象时有重复代码
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}

工厂模式

  1. 通过工厂函数动态创建对象并返回
  2. 使用场景:需要创建多个对象
  3. 问题:对象没有具体类型,都是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}

自定义构造函数模式

  1. 自定义构造函数,通过new创建对象
  2. 使用场景:需要创建多个同一类型的对象
  3. 问题:每个对象都有相同的数据 (方法),浪费内存,需要放到原型对象中
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}

继承模式

原型链继承

原理:子类型的原型为父类型的一个实例对象

  1. 定义父类型构造方法
  2. 给父类型的原型添加方法
  3. 定义子类型构造函数
  4. 创建父类型对象,并赋值给子类型的原型
  5. 将子类型原型的构造属性 (constructor) 设置为子类型
  6. 给子类型原型添加方法
  7. 创建子类型对象:继承父类型的方法
// 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]

借用构造函数继承

  1. 定义父类型构造函数
  2. 定义子类型构造函数
  3. 在子类型构造函数中调用父类型构造函数
function Person(name, age) {
    this.name = name;
    this.age = age;
}

function Student(name, age, grade) {
    Person.call(this, name, age);
    this.grade = grade;
}

组合继承

结合上面两种方法:

  1. 利用原型链继承父类的方法
  2. 利用父类型构造函数,为子类型初始化相同的属性
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请求

定时器

  • 定时器不能保证真正的定时执行,一般会延迟一点时间
  • 定时器的回调函数在主线程中执行
  • 定时器的实现:事件循环模型
posted @ 2022-03-08 15:42  lv6laserlotus  阅读(26)  评论(0)    收藏  举报