前端基础(一)——javascript(含ES6)

142 阅读10分钟

慢慢补充中,笔者因为最近比较忙,先整理薄弱点,牛客前端课程的听课笔记,仅做分享,方便学习,侵删。如果对你有帮助,希望能够给笔者一个赞鼓励笔者继续分享,感谢!

ES5

1.数据类型

有 7 种原始类型,1 种引用类型

  1. Boolean
  2. null
  3. undefined
  4. Number
  5. BigInt
  6. String
  7. Symbol
  8. Object

2.判断数据类型的几种方法

2.1 typeof

  • 对于基本类型,除 null 以外,均可以返回正确的结果
  • 对于引用类型,除 function 以外,一律返回 object 类型
  • 对于 null,返回 object 类型
  • 对于 funciton,返回 function 类型
typeof null === 'object'

const func = () => {}
typeof func === 'function'

2.2 instanceof

检测某一个实例的原型链上是否有这个类的原型属性

  • 可以区分复杂数据类型
  • 只要在当前实例的原型链上,我们用其检测出来的结果就是 true
  • 不能检测基本类型

手写instanceof

function myinstanceof(left, right) {
  let proto = left.__proto__;
  let prototype = right.prototype;
  while (true) {
    if (proto == null) return false;
    if (proto == prototype) return true;
    proto = proto.__proto__;
  }
}

const obj = new Object();
console.log(myinstanceof(obj, Object));

2.3 constructor

用于引用类型,不能检测null、undefined,原型链不会干扰

const num = 123;
console.log(num.constructor === Number);

2.4 Object.prototype.toString.call

适用于所有类型的判断检测

console.log(Object.prototype.toString.call('123')); // [object String]
console.log(Object.prototype.toString.call(123)); // [object Number]
console.log(Object.prototype.toString.call(true)); // [object Boolean]
console.log(Object.prototype.toString.call([1, 2, 3])); // [object Array]
console.log(Object.prototype.toString.call(null)); // [object Null]
console.log(Object.prototype.toString.call(undefined)); // [object Undefined]
console.log(Object.prototype.toString.call({name: 'Jack'})); // [object Object]
console.log(Object.prototype.toString.call(() => {})); // [object Function]
console.log(Object.prototype.toString.call(new Date())); // [object Date]
console.log(Object.prototype.toString.call(/\d/)); // [object RegExp]
console.log(Object.prototype.toString.call(Symbol('123'))); // [object Symbol]

2.赋值、深浅拷贝

浅拷贝方法:

  1. Object.assign
  2. lodash.clone
  3. 展开运算符 ...
  4. Array.prototype.concat
  5. Array.prototype.slice

深拷贝方法:

  1. JSON.parse(JSON.stringify()),但不能处理函数(null)和正则(空对象)
  2. lodash.cloneDeep
  3. jQuery.extend
const $ = require('jquery');
const obj1 = { ... }
const obj2 = $.extend(true, {}, obj1); // ture表示深拷贝
  1. 手写深拷贝
function deepClone(obj, hash = new WeakMap()) {
  if (obj === null) return obj;
  if (obj instanceof Date) return new Date(obj);
  if (obj instanceof RegExp) return new RegExp(obj);
  if (typeof obj !== 'object') return obj;
  if (hash.get(obj)) return hash.get(obj);
  let cloneObj = new obj.constructor();
  hash.set(obj, cloneObj);
  for (let key in obj) {
      if (obj.hasOwnProperty(key)) {
          cloneObj[key] = deepClone(obj[key], hash):
      }
  }
  return cloneObj;
}

let obj = { name: 1, address: { x: 100 } };
obj.o = obj;
let d = deepClone(obj);
obj.address.x = 200;
console.log(d):

3.垃圾回收方式

3.1 栈中的垃圾回收方式

当一个函数执行结束之后,JS引擎会通过向下移动 ESP 来销毁该函数保存在栈中的执行上下文

3.2 堆中的垃圾回收方式

  1. 标记
  2. 回收
  3. 内存整理

3.2.1 新生区垃圾回收

  • 通常为小对象
  • 比较频繁
  • 通常存储容量在1 - 8M
  • 两次垃圾回收后还存活的对象,会被移动到老生区中
  • 使用 Scavenge 算法来处理垃圾回收

Scavenge 算法

  1. 垃圾标记
  2. 存活对象复制到空闲区域
  3. 对象区域与空闲区域角色翻转

3.2.2 老生区垃圾回收

特点:

  1. 占用空间大
  2. 存活时间长

过程:

  1. 标记-清除算法,只标记清除不整理,会产生大量不连续的内存碎片
  2. 标记-整理算法
    • 标记
    • 整理,将所有存活的对象都向内存的一端移动
    • 清除,清除掉端边界以外的内存

3.2.3 全停顿

因为 JS 是运行在主线程之上的,一旦执行垃圾回收算法,JS 的执行就会暂停,待垃圾回收完毕后再恢复脚本执行。这种行为被叫做全停顿

增量标记: 将标记过程分为一个个子过程,与 JS 应用逻辑交替执行,之后清除、整理。

4.避免内存泄漏的方式

  1. 尽可能少的创建全局变量
  2. 手动清除定时器
  3. 少用闭包
  4. 清除 DOM 引用
  5. 使用弱引用类型

5.继承

继承就是一个对象可以访问另外一个对象中的属性和方法,在 JS 中,我们通过原型和原型链的方式来实现了继承特性。

5.1 原型

JS 的每个对象都包含了一个隐藏属性__proto__,即为该对象的原型,该对象可以直接访问其原型对象的方法或属性。

5.2 原型链

查找不是对象的本身属性的路径称为原型链。

5.3 构造函数如何创建对象?

function DogFactory(type, color) {
    this.type = type;
    this.color = color;
}
const dog = new DogFactory('Dog', 'Black');

// 创建实例的过程
// const dog = {};
// dog.__proto__= DogFactory.prototype;
// DogFactory.call(dog, 'Dog', 'Black');

5.4 继承的方式

5.4.1 原型链继承

原理: 将子类的原型指向父类的实例

优点:

  • 父类新增的原型方法或属性,子类都可以访问
  • 简单容易实现

缺点

  • 不能实现多重继承
  • 来自原型对象的所有属性被所有实例共享
  • 创建子类实例时,无法向父类构造函数传参
// 父类型
function Person(name, age) {
    this.name = name;
    this.age = age;
    this.play = [1, 2, 3];
    this.setName = function() {}
}
Person.prototype.setAge = function() {}
// 子类型
function Student(price) {
    this.price = price;
    this.setScore = function() {}
}
Student.prototype = new Person('Wang', 23);
// maybe: 缺少修复构造函数
const s1 = new Student(15000);
const s2 = new Student(14000);
console.log(s1, s2);

5.4.2 借用构造函数实现继承

原理: 在子类构造函数中通过call调用父类构造函数

优点:

  • 解决了原型链继承中子类实例共享父类引用属性的问题
  • 创建子类实例时,可以向父类传递参数
  • 可以实现多重继承(call多个父类构造函数)

缺点:

  • 实例并不是父类的实例,只是子类的实例
  • 只能继承父类的实例属性和方法,不能继承父类原型的属性和方法
  • 无法实现函数复用,每个子类都有父类实例函数的副本,影响性能
function Person(name, age) {
    this.name = name;
    this.age = age;
    this.setName = function() {}
}
Person.prototype.setAge = function() {}
function Student(name, age, price) {
    Person.call(this, name, age);
    // 相当于:this.Person(name, age);
    // this.name = name;
    // this.age = age;
    // this.setName = function() {}
    this.price = price;
}
const s1 = new Student('Tom', 20, 15000);

5.4.3 原型链 + 借用构造函数的组合继承

原理: 通过调用父类构造函数,继承父类属性并保留传参的优点,然后通过将父类实例作为子类原型,实现函数复用。

优点:

  • 可以继承实例属性和方法,也可以继承原型属性和方法
  • 不存在引用属性共享问题
  • 可传参
  • 父类原型上的函数可复用

缺点:

  • 调用了两次父类构造函数,生成了两份实例
function Person(name, age) {
    this.name = name;
    this.age = age;
    this.setAge = function() {}
    console.log(1);
}
Person.prototype.setAge = function() {
    console.log('111');
}
function Student(name, age, price) {
    Person.call(this, name, age);
    this.price = price;
    this.setScore = function() {}
}
Student.prototype = new Person();
// 组合继承需要修复构造函数指向
Student.prototype.constructor = Student;
const s1 = new Student('Tom', 20, 15000);
const s2 = new Student('Jack', 22, 14000);
console.log(s1, s2);

5.4.4 class 关键字

原理: ES6 中引入了 class 关键字,class 可以通过 extends 实现继承,还可以通过 static 定义类的静态方法,这比 ES5 的通过修改原型链实现继承,要清晰和方便很多。

优点:

  • 语法简单易懂,操作更方便

缺点:

  • 并不是所有的浏览器都支持class关键字
class Person {
    // 调用类的构造方法
    constructor(name, age) {
        this.name = name;
        this.age = age;
    }
    // 定义一般的方法
    showName() {
        console.log('调用父类的方法');
        console.log(this.name, this.age);
    }
}
const p1 = new Person('Kobe', 39);
console.log(p1);

// 定义一个子类
class Student extends Person {
    constructor(name, age, salary) {
        super(name, age); // 通过super调用父类的构造函数
        this.salary = salary;
    }
    showName() {
        // 子类定义自己的方法
        console.log('调用子类的方法');
        console.log(this.name, this.age, this.price);
    }
}
const s1 = new Student('Wade', 38, 10000);
console.log(s1);
s1.showName();

扩展阅读,参考JavaScript常用八种继承方案

ES6

介绍一些 ES6 常用的新特性

6. let、const、var

6.1 var 关键字

var 是函数作用域

// 声明的全局变量挂载在window对象下
var a = 1;
console.log(window.a);

// 变量提升
console.log(a);
var a = 1;

// 声明的变量可以重复声明和修改
var a = 1;
var a = '111';
console.log(a);

6.2 let 和 const 关键字

  • let 和 const 声明的全局变量不挂载在window对象下
  • let 和 const 定义的变量不会出现变量提升现象
    • let 和 const 也存在变量声明提升,只是没有初始化分配内存
    • 创建一个变量有三个操作,声明(提到作用域顶部)、初始化(赋默认值)、赋值
    • 其实就是创建提升,因为没有初始化分配内存,所以使用该变量就会报错,这是暂时性死区
  • let 和 const 是 JS 中的块级作用域
  • let 和 const 不允许重复声明(会抛出错误)
  • const 声明一个只读的常量。一旦声明,常量的值就不能改变(如果声明是一个对象,那么不能改变的是对象的引用地址)
// var 变量提升
console.log(a);
var a = 1;

// const, let 暂时性死区
const b = 1;
{
    console.log(b);
    const b = 2;
}
if (true) {
    var a = 1; // 变量会提升到全局
}
console.log(a);
if (true) {
    let b = 1;
}
console.log(b);
const a = 1;
a = 2;

6.3 解构符号

6.3.1 基本使用

let [a, b, c] = [1, 2, 3];
console.log(a, b, c);

6.3.2 嵌套使用

// 数组
let [a, [[b], c] = [1, [[2], 3]];
console.log(a, b, c);

// 对象
let obj = {p: ['hello', {y: 'world'}]};
let {p: [x, {y}]} = obj;
console.log(x, y);

6.3.3 忽略

// 数组
let [a, , b] = [1, 2, 3];
console.log(a, b);

// 对象
let obj = {p: ['hello', {y: 'world'}]};
let {p: [x, {}]} = obj;
console.log(x);

6.3.4 不完全解构

// 数组
let [a = 1, b] = [];
console.log(a, b);

// 对象
let obj = {p: [{y: 'world'}]};
let {p: [{y}, x]} = obj;
console.log(x, y);

6.3.5 剩余运算符

// 数组
let [a, ...b] = [1, 2, 3];
console.log(a, b);

// 对象
let {a, b, ...rest} = {a: 10, b: 20, c: 30, d: 40};
console.log(a, b, rest);

6.3.6 字符串

let [a, b, c, d, e] = 'hello'
console.log(a, b, c, d, e);

6.3.7 解构默认值

// 当匹配结果为undefined时,会触发默认值作为返回结果
let [a = 2] = [undefined];
console.log(a);

let {a = 10, b = 5} = {a: 3};
console.log(a, b);

6.3.8 交换变量的值

let a = 1, b = 2;
[a, b] = [b, a];
console.log(a, b);

6.3.9 解构赋值的应用

// 1.浅克隆与合并
const name = {name: 'aaa'};
const age = {age: 'bbb'};
const person = {...name, ...age};
console.log(person);
// 数组
const a = [1, 2, 3];
const b = [4, 5];
const c = [...a, ...b];
console.log(c);

// 2.提取JSON数据
const jsonData = {id: 10, status: 'OK', data: [111, 222]};
const {id, status, data: numbers} = jsonData;
console.log(id, status, numbers);

// 3.函数参数的定义
// 参数有序
function func1([a, b, c]) { console.log(a, b, c); }
func1([1, 2, 3]);
// 参数无序
function func2({x, y, c}) { console.log(a, b, c); }
func2({z: 3, y: 2, x: 1});
// 参数有默认值
function func3([a = 1, b]) { console.log(a, b); }
func3([, 3]);

6.4 箭头函数

1. 箭头函数没有 this,this 是从外部获取的

const group = {
    title: 'Our Group',
    students: ['John', 'Pete', 'Alice'],
    showList() {
        this.students.forEach(name => alert(this.title + ': ' + name));
    }
}
group.showList();

2. 不能对箭头函数进行 new 操作

不具有 this 自然也就意味着另一个限制:箭头函数不能用作构造器(不能用 new 调用它们)

const Foo = () => {}
const foo = new Foo(); // TypeError: Foo is not a constructor

3. 箭头函数没有arguments

箭头函数没有自己的 arguments 对象,但是箭头函数可以访问外围函数的 arguments 对象

function constant() {
    return () => arguments[0];
}
const result = constant(1);
console.log(result());

4. 箭头函数没有原型

箭头函数没有原型,在其内使用 this、arguments、new.target,由外围最近一层非箭头函数决定.

// 箭头函数没有原型
const Foo = () => {};
console.log(Foo.prototype); // undefined

// 箭头函数不能 new
const Foo = () => {};
const foo = new Foo(); // TypeError

// 箭头函数没有this
/*const group = {
    title: 'Our Group',
    students: ['John', 'Pete', 'Alice'],
    showList() {
        this.students.forEach(function(stu) {
            alert(this.title + ': ' + stu);
        });
    }
};*/
const group = {
    title: 'Our Group',
    students: ['John', 'Pete', 'Alice'],
    showList() {
        this.students.forEach(stu => alert(this.title + ': ' + stu));
    }
};
group.showList();

// 箭头函数没有 arguments
function constant() {
    return () => arguments[0];
}
const result = constant(1);
console.log(result());

6.5 Map 与 WeakMap

6.5.1 Map 特点

  • Map 的键和值可以是任何数据类型
  • 键值对按照插入顺序排列
  • Map 数据可迭代,Object 数据不可迭代
// Map 的键和值可以是任何数据类型
const mp = new Map();
const o = { num: 1 };
mp.set(o, '123');
console.log(mp);

// Map 键值对按照插入顺序排列
const mp = new Map([[1, '111'], [2, '222']]);
console.log(mp);
// Object 自动排序
const obj = {'2': '111', '1': '222'};
console.log(obj);

// Map 数据可迭代,Object 数据不可迭代
const map = new Map([[1, '111'], [2, '222']]);
console.log(map);
for (const [key, value] of map) console.log(key, value);
const obj = {'2': '111', '1': '222'};
console.log(obj);
for (const [key, value] of obj) console.log(key, value);

6.5.2 Map 的常用属性

// api
map.set(key, value);
map.get(key);
map.delete(key);
map.clear();
map.entries();
map.has(key);
map.keys();
map.values();
map.size();

// 遍历
const map = new Map([['a', 1], ['b', 2]]);
for (const key of map.keys()) console.log(key);
for (const value of map.values()) console.log(value);
for (const item of map.entries()) console.log(item);
// 等价于 for-of
for (const [key, value] of map.entries()) console.log(key, value);

6.5.3 WeakMap 特点

  • 只接受对象作为键名(null 除外),不接受其他类型
  • 键值可以是任意的
  • 键名是弱引用,键名所指向的对象可以被垃圾回收,此时键名是无效的
  • 不可枚举,方法有 get、set、has、delete
// 只能使用对象作为键名
const weakMap = new WeakMap();
weakMap.set('a', 111); // Error

// 键名是弱引用,如果对象销毁,该属性就销毁
const weakMap = new WeakMap();
let o = { num: 1 };
weakMap.set(o, 111);
o = null;
console.log(weakMap);

// 不能遍历
const weakMap = new WeakMap();
let o = { num: 1 };
weakMap.set(o, 111);
for (const item of weakMap) console.log(item); // Error