【知识梳理】2.JavaScript_5.25

459 阅读8分钟

JS

数据类型

基本类型(存栈):Bboolean、Number、String、Undefined、Null、Symbol、BigInt

引用类型(存堆):Object

类型判断

  1. typeof,用于除 null 外的基本类型

注意:

typeof NaN === "number";
  1. instanceOf,基于原型链查找

  2. Array.isArray(),用于判断数组

  3. 终极判断 Object.prototype.toString.call()

"[object Array]";
  1. 判断是否为空对象
var obj = {};

Object.prototype.toString.call(obj) &&
  Object.getOwnPropertyNames(obj).length === 0;

类型转换

JS中只有三种类型转换:

  • 转数字
  • 转布尔
  • 转字符串

2021-03-13_222407.png

进行==

  • 首先判断两者类型是否相同,如果相等,判断值是否相等
  • 如果类型不同,进行类型转换
    • 判断比较的是否是 null 或者是 undefined, 如果是, 返回 true .
    • 判断两者类型是否为 string 和 number, 如果是, 将字符串转换成 number
    • 判断其中一方是否为 boolean, 如果是, 将 boolean 转为 number 再进行判断
    • 判断其中一方是否为 object 且另一方为 string、number 或者 symbol , 如果是, 将 object 转为原始类型再进行判断

对象转原始类型流程:

  1. toPrimitive()方法
  2. 调用valueOf(),如果转换为原始类型,则返回
  3. 调用toString(),如果转换为原始类型,则返回
  4. 如果都没有返回原始类型,会报错
// 说说'1'.toString()过程
var s = new String('1');
s.toString();
s = null;

// [] == ![]
== 两边转数字再进行比较

// if(a == 1 && a == 2)成立
var a = {
  value: 0,
  valueOf: function() {
    this.value++;
    return this.value;
  }
};
console.log(a == 1 && a == 2)

模拟实现instanceof

原理:基于原型链查找

function instanceof2(left, right) {
  // 基本类型
  if(typeof left !== 'object' || left === null) {
    return false;
  }
  // 获取left的原型
  let _proto = Object.getPrototypOf(left);
  // 原型链查找
  while(true) {
    if(_proto === null ) return false;
    if(_proto === right.prototype) return true;
    _proto = Object.getPrototypOf(_proto);
  }
}
// 测试实例
let obj = new Object();
instanceof2(obj, Object);

概念理解

变量提升和暂时性死区

变量提升就是变量在声明之前就可以使用,值为undefined。

暂时性死区的本质就是,只要一进入当前作用域,所要使用的变量就已经存在了,但是不可获取

原型和原型链

每个函数都有一个prototype属性,这个属性是一个对象,这个对象指向了能被所有该构造函数实例共享的属性和方法。这个对象我们称为实例的原型

每一个JavaScript对象(除了null)都具有的一个属性,叫__proto__,这个属性会指向该对象的原型

// 获取对象的原型
1. obj.constructor.prototype
2. obj.__proto__
3. Object.getPrototypeOf()

当我们访问一个对象的属性时候,会先在这个对象本身上找,如果没有没有会通过__proto__查找这个对象的原型,如果原型没有会再通过__proto__查找原型的原型,直到Object.prototype,这个过程就叫原型链

原型和原型链

作用域

规定了如何查找变量,也就是当前执行代码对变量的访问权限。JavaScript采用静态作用域,也就是函数的作用域在函数定义的时候就决定了。

动态作用域则是在函数执行时才决定。

var name = 'a';
function foo() {
  console.log(name);
}
function bar() {
  var name = 'b';
  foo();
}
bar();  // 'a'

执行上下文

执行一段可执行代码(函数、全局、eval)时,所做的准备工作。包括:

  • 变量对象
  • 作用域链
  • this

变量对象

变量对象是与执行上下文相关的数据作用域,存储了在上下文中定义的变量和函数声明。

  • 全局上下文的变量对象初始化是全局对象
  • 在进入执行上下文时会给变量对象添加形参、函数声明、变量声明等初始值
  • 在代码执行阶段,会再次修改变量对象的属性值

先处理函数声明,再处理变量声明

console.log(foo); // function foo

function foo(){
  console.log("foo");
}

var foo = 1;

作用域链

当查找变量的时候,会先从当前上下文的变量对象中查找,如果没有就会从父级中查找。这样由多个执行上下文的变量对象构成的链表就构成了作用域链

当函数创建时,会有一个名为 [[scope]] 的内部属性保存所有父变量对象

this

只在执行时确定,指向调用的对象

  • 在函数中
  • 作为对象方法
  • 作为构造函数
  • 使用call bind apply
  • 箭头函数中的this,继承它上一层中的this

闭包

能够访问既不是函数参数也不是函数局部变量的函数

var data = [];

for (var i = 0; i < 3; i++) {
  data[i] = function () {
    console.log(i);
  };
}

data[0]();
data[1]();
data[2]();
var data = [];

for (var i = 0; i < 3; i++) {
  data[i] = (function (i) {
        return function(){
            console.log(i);
        }
  })(i);
}

data[0]();
data[1]();
data[2]();

call/apply

func.call(thisArg, arg1, arg2, ...)
func.apply(thisArg, [argsArray])

call和apply使用一个指定的this和若干个指定的参数值(apply的参数则是一个数组或类数组)的前提下执行某个函数

Function.prototype.call2 = function (context, ...args) {
  // 判断浏览器还是Node环境
  if(!context) {
    context = typeof window === 'undefined' ? global : window;
  }
  // 将函数作为当前上下文context的方法并执行
  context.fn = this;
  var result = context.fn(...args);
  delete context.fn;
  return result;
}
Function.prototype.apply2 = function (context, args) {
  // 判断浏览器还是Node环境
  if(!context) {
    context = typeof window === 'undefined' ? global : window;
  }
  context.fn = this;
  let result = typeof args? context.fn() : context.fn(...args);
  delete context.fn;
  return result;
}

bind

bind返回一个函数,bind的第一个参数作为this,可以传入参数

返回的新函数不是一个普通的function,而是一个bound function,简称绑定函数

当执行fn1时,本质上等于window.fn1(),如果this还能被改变,那this岂不是得指向window,那bind方法就没太大意义了

参考

Function.prototype.bind2 = function(context) {
  if(typeof this !== 'function'){
    throw new TypeError('need a function');
  }

  let self = this;
  // bind传递的函数参数
  let _args = [...arguments].slice(1);
  function Fn() {};
  let bound = function() {
    let args = [..._args, ...arguments];
    context = this instanceof Fn ? this : context;
    // 绑定this和参数
    return self.apply(context, args);
  }
  // 绑定原型链
  Fn.prototype = this.prototype;
  bound.prototype = new Fn();
  return bound;
}
// 测试实例
var name = 'Jack';
function person(age){
  console.log(this.name , age);
}
person();

let result = person.bind({name: 'cc'}, 23);
result();

new

  1. 新生成了一个对象
  2. 对象连接到构造函数原型上
  3. 执行构造函数代码,并绑定this到这个对象
  4. 返回新对象

注:当在构造函数中返回一个对象时,内部创建出来的新对象就被我们返回的对象所覆盖

function new2() {
  // 新生成了一个对象
  let obj = {};
  let [constructor, ...args] = [...arguments];
  if (typeof constructor !== 'function') {
    throw 'this param must be a function'
  }
  // 对象连接到构造函数原型上
  obj.__proto__ = constructor.prototype;
  // 执行构造函数代码,并绑定this到这个对象
  let res = constructor.apply(obj, args);
  // 判断构造函数返回的是否为对象
  if ((typeof res === 'object' && res !== null) || typeof res === 'function') {
    return res;
  }
  return obj;
}
// 测试实例
function Persion(name, age) {
  this.name = name;
  this.age = age || 23;
}
Persion.prototype.sex = true;
let p = new2(Persion, 'hh');

继承

原型链继承

function Parent () {
  this.name = 'kevin';
}

Parent.prototype.getName = function () {
  console.log(this.name);
}

function Child () {

}

Child.prototype = new Parent();

var child1 = new Child();
console.log(child1.getName()) // kevin
引用类型的属性被所有实例共享
创建子类的实例时,不能向父类传参

构造函数继承

function Parent (name) {
  this.name = name;
}

function Child (name) {
  Parent.call(this, name);
}

var child1 = new Child('kevin');
console.log(child1.name); // kevin

var child2 = new Child('daisy');
console.log(child2.name); // daisy
方法都在构造函数中,无法复用
子类无法访问父类原型中的方法

组合继承

function Parent(name) {
  this.name = name;
  this.colors = ['red', 'blue', 'green'];
}

Parent.prototype.getName = function () {
  console.log(this.name)
}
// 子类构造函数中调用父类构造函数
function Child(name, age) {
  Parent.call(this, name);
  this.age = age;
}

Child.prototype = new Parent();
Child.prototype.constructor = Child;

var child1 = new Child('kevin', '18');

父类的构造方法会调用2次

寄生组合式继承

function Parent(name) {
  this.name = name;
  this.colors = ['red', 'blue', 'green'];
}

Parent.prototype.getName = function () {
  console.log(this.name)
}
// 子类构造函数中调用父类构造函数
function Child(name, age) {
  Parent.call(this, name);
  this.age = age;
}

Child.prototype = Object.create(Parent.prototype);
Child.prototype.constructor = Child;

var child2 = new Child('kevin', '18');

注:ES6中extends使用babel编译到ES5,使用的也是寄生组合式继承

EventLoop

相关文章尝试弄懂JavaScript的运行机制

console.log('1');
setTimeout(function() {
  console.log('2');
  process.nextTick(function() {
    console.log('3');
  })
  new Promise(function(resolve) {
    console.log('4');
    resolve();
  }).then(function() {
    console.log('5')
  })
})
process.nextTick(function() {
  console.log('6');
})
new Promise(function(resolve) {
  console.log('7');
  resolve();
}).then(function() {
  console.log('8')
})
setTimeout(function() {
  console.log('9');
  process.nextTick(function() {
    console.log('10');
  })
  new Promise(function(resolve) {
    console.log('11');
    resolve();
  }).then(function() {
    console.log('12')
  })
})

XHR和fetch

xhr和fetch都是JavaScript原始提供的异步请求接口,fetch实现了Promise API。

Ajax是jQuery对xhr的封装,axios也是对xhr的封装,但它实现了Promise API。

高阶函数

一个函数他的参数或者返回值是另一个函数的函数

参考

ES6

let和const

默认使用const,只有在确实需要修改变量的时候,才使用let

let和var区别:

1、let块级作用域,var只有函数和全局作用域

2、let不存在变量提升

3、相同作用域不允许重复声明,var的话可以但是会覆盖

变量的解构赋值

从数组和对象中提取值,对变量进行赋值。如果解构不成功,变量的值就会被赋为undefined

let [foo] = []; // foo = undefined
let [cdc, foo, nsa] = [1]; // cdc = 1,foo = undefined, nas = []

字符串拓展

1.模板字符串

const foo = 'my name is' + exp;
// 等于
const foo = `my name is ${exp}`

2.标签模板

标签模板其实不是模板,而是函数调用的一种特殊形式。“标签”指的就是函数,紧跟在后面的模板字符串就是它的参数。

alert`hello`
// 等同于
alert(['hello'])

函数拓展

默认值

function test(quantity) {
  const q = quantity || 1;
  return q;
}
// 等于
function test(quantity = 1) {
  return quantity;
}

箭头函数

1、函数体内的this是定义时的父级,不是调用时的对象

2、函数体内不存在arguments,用rest替代

3、不能用作构造函数

数组拓展

实例的find()findIndex()

参数都是回调函数,用于找到第一个符合的成员(成员位置)

[1, 4, -5, 10].find((n) => n < 0)
// -5

实例的keys()values()

for (let index of ['a', 'b'].keys()) {
  console.log(index);
}
// 0
// 1

for (let elem of ['a', 'b'].values()) {
  console.log(elem);
}
// 'a'
// 'b'

for (let [index, elem] of ['a', 'b'].entries()) {
  console.log(index, elem);
}
// 0 "a"
// 1 "b"

拓展运算符

...

let obj1 = { a: 1, b: 2,c: 3 }
let obj2 = { b: 4, c: 5, d: 6}
let merged = {...obj1, ...obj2};

双冒号

foo::bar;
// 等同于
bar.bind(foo);

foo::bar(...arguments);
// 等同于
bar.apply(foo, arguments);

Symbol

唯一值、魔法字符串、模拟私有变量

1、作为对象的属性key,但不能被.keys和for in遍历

2、作为常量

3、和模块化一起使用模拟私有属性

参考

Set和Map

Set

set

类似于数组,但是成员的值都是唯一的

向 Set 加入值的时候,不会发生类型转换,所以5和"5"是两个不同的值

属性: size

方法: add() delete() has() clear()

weakset

  • WeakSet的成员只能是对象
  • WeakSet中的对象都是弱引用

Map

map

Object,本质上是键值对的集合。是传统上只能用字符串当作键

const obj = new Map();
obj.set(34, 'asdf');
obj.get(34);

const myMap = new Map()
  .set(true, 7)
  .set({foo: 3}, ['abc']);

weakMap

  • 只接受对象作为键名(null除外)
  • 它的键名所引用的对象都是弱引用

Promise

const promise = new Promise(function(resolve, reject) {
  // ... some code

  if (/* 异步操作成功 */){
    resolve(value);
  } else {
    reject(error);
  }
});

promise.then(function(value) {
  // success
}, function(error) {
  // failure
});

Generator

*标记一个函数为协程,yield表示执行到此处,执行权将交给其他协程

协程遇到yield命令就暂停,等到执行权返回,再从暂停的地方继续往后执行

Iterator

接口,为JS中表示集合的数据结构提供遍历的操作

原生具有Iterator接口的数据结构:Array、String、Map、Set、arguments、NodeList

function iterator2(arr) {
  var nextIndex = 0;

  return {
    next: function() {
      return nextIndex < arr.length ? {value: arr[nextIndex++]} : {done: true}
    }
  }
}

async/await

generator依靠执行器co,async自带执行器。是对generator的实现

Class

class中方法不可枚举,不存在变量提升

class中子类构造函数调用super

Decorator

参考