js基础知识梳理


2020-9-25 js 基础知识 梳理

面向对象

1. 面向对象三要素
什么是面向对象?面向对象程序设计(Object Oriented Programming,OOP)是一种计算机编程架构,面向对象将问题抽象出具体的对象,而这个对象有自己的属性和方法,在解决问题的时候将不同的对象组合在一起使用,使得代码的重用性、灵活性和扩展性增强。 JavaScript 本身就是面向对象的,只是它实现面向对象的方式和主流的流派不太一样。JavaScript 是基于原型的面向对象系统, 而其它语言大多是基于类去描述面向对象的。 js面向对象主要特征有:封装、继承、多态

  • 封装
    把客观事物封装成抽象的类,隐藏属性和方法的实现细节,仅对外公开接口, 也就是说,封装就是将属性和方法组成一个类的过程就称之为封装。
  • 继承
    原型链继承
//多个实例对引用类型操作会被篡改
 function Animal() {
    this.name = 'cat'
    this.msg = {
      age: 9
    }
  }
  Animal.prototype.greet = function () {
    console.log('hehe')
  }
  function Dog() {
    this.name = 'dog'
  }
  Dog.prototype = new Animal()  //核心一步

  const a = new Dog()
  a.msg.age = '99'
  const b = new Animal()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

构造函数继承

//只能继承父类的实例属性和方法,不能继承原型属性/方法。性能不好,每个子类都会拥有父类实例的副本。
function Animal() {
    this.name = 'cat'
    this.msg = {
      age: 9
    }
  }
  Animal.prototype.greet = function () {
    console.log('hehe')
  }
  function Dog() {
   Animal.call(this)            // 核心一步
  }
const a=new Dog()
1
2
3
4
5
6
7
8
9
10
11
12
13
14

组合继承

//将上两种方法结合起来
function Animal() {
    this.name = 'cat'
    this.msg = {
      age: 9
    }
  }
  Animal.prototype.greet = function () {
    console.log('hehe')
  }
  function Dog() {
   Animal.call(this)            // 核心一步
  }
  Dog.prototype = new Animal()  // 核心一步
const a=new Dog()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

原型式继承:利用一个空对象作为中介,将某个对象直接赋值给空对象构造函数的原型

//不能做到函数复用 共享引用类型属性的值 无法传递参数
function inheritObject(obj){
    function F(){};
    F.prototype = obj;
    return new F();
}

var situation = {
    companies:['bigo','yy','uc'];
    area:'guangzhou';
}

var situationA = inheritObject(situation);
console.log(situationA.area)     //'guangzhou'


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

寄生式继承:在原型式继承的基础上,增强对象,返回构造函数.

//不能做到函数复用 共享引用类型属性的值 无法传递参数
 function createAnother(original){
  var clone = object(original); // 或 Object.create(original) 
  clone.sayHi = function(){  // 以某种方式来增强对象
    alert("hi");
  };
  return clone; // 返回这个对象
}
var person = {
  name: 'Nicholas',
  friends : ["Shelby","Coury","Van"]
}

var anotherPerson  = createAnother(person) 
1
2
3
4
5
6
7
8
9
10
11
12
13
14

class类实现继承(es6)

class A {
}

class B extends A {
  constructor() {
    super();
  }
}
1
2
3
4
5
6
7
8

子类B在constructor中的super指向父类的构造函数。在继承时一定要调用,这是必须的,否则 JavaScript 引擎会报错。 虽然是调用父类的构造函数但是super内部的this指向的是B

super作为对象时,在普通方法中,指向父类的原型对象;在静态方法中,指向父类。

class Parent {
  static myMethod(msg) {
    console.log('static', msg);
  }

  myMethod(msg) {
    console.log('instance', msg);
  }
}

class Child extends Parent {
  static myMethod(msg) {
    super.myMethod(msg);
  }

  myMethod(msg) {
    super.myMethod(msg);
  }
}

Child.myMethod(1); // static 1

var child = new Child();
child.myMethod(2); // instance 2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

其他介绍

2. 面向过程
面向过程侧重去解决一个问题的具体步骤,先做什么再做什么,一步一步调用函数。

3. 面向对象和过程的联系和区别
面向对象和面向过程的侧重点是不同的,面向对象侧重将解决一个问题抽象成一个对象,对象有自己的属性和方法,以功能来划分问题;面向过程侧重去解决一个问题的具体步骤,先做什么再做什么,一步一步调用函数。
举个栗子:“把大象放冰箱” 用面向过程和面向对象的不同实现思路:
(1) 面向过程:
开门(冰箱)
装进去(冰箱,大象)
关门(冰箱)
(2) 面向对象:
冰箱.开门()
冰箱.装进去(大象)
冰箱.关门()

数据类型

1. 基础数据类型
7大原始类型与Object类型
string: 由多个16位Unicode字符组成的字符序列,有单引号或双引号表示
number: 表示整数和浮点数值
bigint: 新增的一种内置对象,它提供了一种方法来表示大于 2^53 - 1 的整数,表示任意大的整数,可以用在一个整数字面量后面加 n 的方式定义一个 BigInt ,如:10n,或者调用函数BigInt()
boolean: 有两个字面值,true和false
null: 只有一个值的数据类型,值为null.表示一个空对象指针,但用typeof操作会返回一个对象。一般 我们把将来用于保存对象的变量初始化为null.
undefined: 这个类型只有一个值,在声明变量未进行赋值时,这个变量的值就是undefined.
Symbol: 唯一的值。

JS中有三个基本数据类型是比较特殊的存在,分别是String、number、Boolean,这个三个基本数据类型有自己对应的包装对象。包装对象的设计目的,首先是使得“对象”这种类型可以覆盖 JavaScript 所有的值,整门语言有一个通用的数据模型,其次是使得原始类型的值也有办法调用自己的方法。在对基本类型数据进行操作时,首先会创建一个相应的对象代替,操作完之后并删除, 这就是我们可以直接使用'123'.split()等方法

new String('123') // String {"123"} 包装对象 包含String的属性和方法
new Number(123) // Number {123} 
new Boolean(123)  // Boolean {true} 
1
2
3

Number、String和Boolean这三个原生对象,如果不作为构造函数调用(即调用时不加new),而是作为普通函数调用,将返回转换后的基本类型值,常常用于将任意类型的值转为数值、字符串和布尔值

// 字符串转为数值
Number('123') // 123

// 数值转为字符串
String(123) // "123"

// 数值转为布尔值
Boolean(123) // true
1
2
3
4
5
6
7
8

2. 引用数据类型
不同于基本数据类型的原始值是直接保存在变量中,而引用类型的值存在于堆内存中,变量是一个指向实际对象所在位置的指针(或者说引用)

var name = 'kk';
var city = 'chengdu';

var persion = { name: 'kk', city: 'chengdu' }
1
2
3
4
name: 'kk'
city: 'chengdu'
persion: xxxxxx内存地址---> { name: 'kk', city: 'chengdu' }

3. 类型判断方法

  • 原始类型判断
    原始类型string、number、undefined、boolean、symbol、bigint都能通过typeof(返回字符串形式)直接判断类型,还有对象类型function也可判断
    除了null无法通过typeof(为object)直接判断类型(历史遗留),包括对象类型,typeof把null当作对象类型处理,所以typeof无法判断对象类型,typeof也能判断function
typeof '123'; // "string"

typeof undefined; // "undefined"

typeof 1 // "number"

typeof false // "boolean"

typeof Symbol() // "symbol"

typeof 1n // "bigint"

typeof null // "object"

typeof function a () {} // "function"

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
  • 非原始类型判断(以及null)
    instanceof: 用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链,所以在类的原型继承中,最后检测出来的结果未必是正确的. 而且instanceof后面必须更一个对象。 不能检测基本类型
var a = [], b = {}; function fn (){};
a instanceof Array; // true
a instanceof Object; // true
b instanceof Object; // true
fn instanceof Function; // true
fn instanceof Object; // true
null instanceof Object; // false

1
2
3
4
5
6
7
8

constructor:每个构造函数的原型对象都有一个constructor属性,并且指向构造函数本身,由于我们可以手动修改 这个属性,所以结果也不是很准确。 不能检测null和undefined

var a = [], function fn (){};
a.constructor === Array; // true
a.constructor === Object; // false
fn.constructor === Function; // true
''.constructor === String; // true
(123).constructor === Number; // true
false.constructor === Boolean // true
...
1
2
3
4
5
6
7
8

Object.prototype.toString.call(最佳方案):

Object.prototype.toString.call('') === "[object String]"; // true

Object.prototype.toString.call([]) === "[object Array]"; // true

Object.prototype.toString.call(function a(){}) === "[object Function]"; // true

Object.prototype.toString.call(null) === "[object Null]"; // true
 
Object.prototype.toString.call(undefined) === "[object Undefined]"; // true

Object.prototype.toString.call(1n) === "[object BigInt]"; // true

Object.prototype.toString.call(Symbol()) === "[object Symbol]" // true
...
1
2
3
4
5
6
7
8
9
10
11
12
13
14

4. 类型转换
javaScript作为一门弱类型语言,本质为一个变量可以被赋予不同的数据类型。代码简洁灵活,但稍有 不慎,会出现很多坑。
javaScript也作为一门动态类型语言,在运行时,可以随便改变其变量的结构。 所以js变量可以做任意的类型转换,有两种方式,显式类型转换和隐式类型转换。 但是能转换的类型只有三种:to Number,to String,to Boolean.
只有'' 0 null undefined NaN false转换boolean为false,其他都为true 当引用类型转换时,就稍微有些复杂,我们来举个例子:(所有对象转换boolean都为true)

let obj={
    value:'你好啊',
    num:2,
    toString:function(){
        return this.value
    },
    valueOf:function(){
        return this.num
    },   
}
console.log(obj+'明天')  //2明天
console.log(obj+1)    // 3
console.log(String(obj))   // 你好啊

1
2
3
4
5
6
7
8
9
10
11
12
13
14

当对象进行类型转换时:
1.首先调用valueOf,如果执行结果是原始值,返回,如果不是下一步
2.其次调用toString,如果执行结果是原始值,返回,如果不是,报错。 特殊情况: 当使用显式类型转换成String时,执行顺序则是先调用toString,其次调用valueOf
显式类型转换:
Number() / parseFloat() / parseInt()/String() / toString()/Boolean()
隐式类型转换: + - == !><= <= >=

先来看看 == 和 === 区别:

对于==的判断:

  • 它会优先对比数据的类型是否一致
  • 不一致则进行隐式转换,一致则判断值的大小,得出结果
  • 继续判断两个类型是否为null与undefined,如果是则返回true
  • 接着判断是否为string与number,如果是把string转换为number再对比大小
  • 判断其中一方是否为boolean,如果是就转为number再进一步判断
  • 判断一方是否为object,另一方为string、number、symbol,如果是则把object转为原始类型再判断
    对于===的判断:
  • ===属于严格判断,直接判断两者类型是否相同,不同则返回false
  • 如果相同再比较大小,不会进行任何隐式转换
  • 对于引用类型来说,比较的都是引用内存地址,所以===这种方式的比较,除非两者存储的内存地址相同才相等,反之false

思考比较有意思的一道面试题:如何使 a==1 && a==2 && a==3 为true? 其他扩展查看

const a = { value : 0 };
a.valueOf = function() {
    return this.value += 1;
};

console.log(a==1 && a==2 && a==3); //true

1
2
3
4
5
6
7

5. 数据拷贝
由于数据存储方式不同,对于引用数据而言,有了浅拷贝和深拷贝。浅拷贝是指拷贝地址,公用同一个堆内存,两个变量相互受影响,深拷贝使指,开辟一块内存空间,保存相同的值。互不受影响。

  • 浅拷贝: Object.assign,contact,扩展运算符等。
  • 深拷贝: 1.JSON.parse(JSON.stringify(obj)) 无法拷贝undefined与symbol属性,无法拷贝循环引用对象 (var a={a:'123'} a.c=a)
    2.简单递归+循环
//简单版深拷贝,只能拷贝基本原始类型和普通对象与数组,无法拷贝循环引用
function simpleDeepClone(a) {
  const b=Array.isArray(a) ? [] : {}
  for (const key of Object.keys(a)) {
    const type = typeof a[key]
    if (type !== 'object' || a[key] === null) {
      b[key] = a[key]
    } else {
      b[key] = simpleDeepClone(a[key])
    }
  }
  return b
}
//精简版深拷贝只能拷贝基本原始类型和普通对象与数组,可以拷贝循环引用
function deepClone(a, weakMap = new WeakMap()) {
  if (typeof a !== 'object' || a === null) return a
  if (s = weakMap.get(a)) return s
  const b = Array.isArray(a) ? [] : {}
  weakMap.set(a, b)
  for (const key of Object.keys(a)) b[key] = clone(a[key], weakMap)
  return b
}
//js原生深拷贝,无法拷贝Symbol、null、循环引用
function JSdeepClone(data) {
  if (!data || !(data instanceof Object) || (typeof data == "function")) {
    return data || undefined;
  }
  const constructor = data.constructor;
  const result = new constructor();
  for (const key in data) {
    if (data.hasOwnProperty(key)) {
      result[key] = deepClone(data[key]);
    }
  }
  return result;
}


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38

对于对象循环引用自己的拷贝比较复杂:

//深拷贝具体版,非完全,但大部分都可以
function deepClonePlus(a, weakMap = new WeakMap()) {
  const type = typeof a
  if (a === null || type !== 'object') return a
  if (s = weakMap.get(a)) return s
  const allKeys = Reflect.ownKeys(a)
  const newObj = Array.isArray(a) ? [] : {}
  weakMap.set(a, newObj)
  for (const key of allKeys) {
    const value = a[key]
    const T = typeof value
    if (value === null || T !== 'object') {
      newObj[key] = value
      continue
    }
    const objT = Object.prototype.toString.call(value)
    if (objT === '[object Object]' || objT === '[object Array]') {
      newObj[key] = deepClonePlus(value, weakMap)
      continue
    }
    if (objT === '[object Set]' || objT === '[object Map]') {
      if (objT === '[object Set]') {
        newObj[key] = new Set()
        value.forEach(v => newObj[key].add(deepClonePlus(v, weakMap)))
      } else {
        newObj[key] = new Map()
        value.forEach((v, i) => newObj[key].set(i, deepClonePlus(v, weakMap)))
      }
      continue
    }
    if (objT === '[object Symbol]') {
      newObj[key] = Object(Symbol.prototype.valueOf.call(value))
      continue
    }
    newObj[key] = new a[key].constructor(value)
  }
  return newObj
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39

6. 每个数据类型的特点、用途

常见的string、number等不再介绍,可结合用途具体使用,这里介绍一下Set和Map

  • Set

Set对象是值的集合,值只会出现一次,元素是唯一的

let mySet = new Set();

mySet.add(1); // Set [ 1 ]
mySet.add(5); // Set [ 1, 5 ]
mySet.add(5); // Set [ 1, 5 ]
mySet.add("some text"); // Set [ 1, 5, "some text" ]
let o = {a: 1, b: 2};
mySet.add(o);

mySet.add({a: 1, b: 2}); // o 指向的是不同的对象,所以没问题

mySet.has(1); // true
mySet.has(3); // false
mySet.has(5);              // true
mySet.has(Math.sqrt(25));  // true
mySet.has("Some Text".toLowerCase()); // true
mySet.has(o); // true

mySet.size; // 5

mySet.delete(5);  // true,  从set中移除5
mySet.has(5);     // false, 5已经被移除

mySet.size; // 4, 刚刚移除一个值

console.log(mySet); 
// logs Set(4) { 1, "some text", {…}, {…} } in Chrome
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
for (let [key, value] of mySet.entries()) console.log(key, value); //键与值相等 
按顺序输出: 
1 1
some text some text
{a: 1, b: 2} {a: 1, b: 2}
{a: 1, b: 2} {a: 1, b: 2}
1
2
3
4
5
6

Set常用于去除重复元素。

// 去除数组的重复成员
[...new Set([1,2,3,1,2,3])] // [1, 2, 3]

// 去除重复字符
[...new Set('abcabc')].join('') // 'abc'

1
2
3
4
5
6
  • Map

Map对象保存键值对。任何值(对象或者原始值) 都可以作为一个键或一个值。它和 JS 对象不同,JS 对象只能用字符串和Symbol作为键,而Map可以使用任何值。

除了键类型上的不同,它和Object还有以下不同:

Map中的键值是有序的,而添加到对象中的键则不是。
Map可以通过size获取键值对个数,而Object的键值对个数只能手动计算。
Map可直接进行迭代,而 Object 的迭代需要先获取它的键数组,然后再进行迭代。
Object都有自己的原型,原型链上的键名有可能和你自己在对象上的设置的键名产生冲突。虽然ES5开始可以用map = Object.create(null) 来创建一个没有原型的对象,但是这种用法不太常见。
Map在涉及频繁增删键值对的场景下会有些性能优势。

const m = new Map();
//.set(key, value) 添加成员
m.set('name', 'nan').set('age', 18).set(symbol = Symbol('test'), 'test');
//.size 成员数量
console.log(m.size)  //2
//.get(key) 读取成员 找不到返回undefined
console.log(m.get(symbol))  //test
//.has(key) 返回一个布尔值
console.log(m.has('age'))  //true
//.delete(key) 删除某个键 返回布尔值
console.log(m.delete('age'))  //true
console.log(m.has('age')) //false
//.clear() 清空Map
m.clear()
console.log(m.size) //0

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const map = new Map([
  [1, 'one'],
  [2, 'two'],
  [3, 'three'],
]);

console.log([...map.keys()])
// [1, 2, 3]
console.log([...map.values()])
// ['one', 'two', 'three']
console.log([...map.entries()])
// [[1,'one'], [2, 'two'], [3, 'three']]
//等同于map.entries()
for (let [key, value] of map) {
  console.log(key, value)
}
// 1 'one'
// 2 'two'
// 3 'three'

map.forEach(function(value, key, map) {
  console.log("Key: %s, Value: %s", key, value);
});
// Key: 1, Value: one
// Key: 2, Value: two
// Key: 3, Value: three

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

7. 数据存储、读取及释放机制

js具有自动垃圾回收机制:按照固定的时间间隔周期性找出那些不再继续使用的变量并释放其占用的内存空间。

  • 标记清除
    变量进入环境(比如在函数中声明一个变量)或离开环境会被标记跟踪,使用何种方式记录不重要,关键在于记录这种变化,然后垃圾回收器会周期性清除那些被标记为‘不再使用’的变量,然后释放其所占用的内存空间。
  • 引用计数
    顾名思义就是记录每个值被引用的次数,当声明一个变量并将一个引用类型值赋值给该变量时,这个值的引用次数就是1,如果同一个值又被赋值给另外一个变量,该值的引用次数加1,相反,对这个值引用的变量取得了另外一个值时,则该值的引用次数减1,当引用次数为0,说明没有变量引用它了,垃圾回收器回收时就会释放该值所占用的内存空间。

    引用计数的弊端:循环引用
function fun () {
    var objA = {};
    var objB = {};

    objA.key1 = objB;
    objB.key2 = objA
}
1
2
3
4
5
6
7

当fun()被调用执行时,如果采取的是引用计数回收策略,由于objA、objB通过各自的属性相互引用,它们的引用次数都是2,内存占用永远不会被回收。在一些情况下,我们可以手动切断值与变量之间的引用,比如obj = null, 从而实现内存释放。

基础知识点

// TODO
1. 变量、常量

2. 作用域、作用域链

3. this、执行上下文

4. 闭包、高阶函数

5. 构造函数、class、继承

6. new 一个构造函数实现的过程

7. 原型、原型链

8.apply、call、bind实现

AMD、CMD、CommonJS与ES6模块化

BOM

// TODO

DOM

// TODO

存储

// TODO

Session

cookie与session的区别

本地存储localStorage与sessionStorage

跨域

缓存

协商缓存与强缓存

Last Updated: 9/26/2020, 5:09:41 PM