JS基础篇-核心概念篇

96 阅读9分钟

核心概念篇

1. BOM和DOM

BOM(Browser Object Model)即浏览器对象模型,它提供了独立于内容而与浏览器窗口进行交互的对象,其核心对象是 window。BOM 比 DOM 更大,它包含 DOM。

截屏2023-10-06 下午8.19.54.png

document对象

Document 对象提供了一些在浏览器服务中作为页面内容入口点而加载的一些页面,也就是 DOM 树。

location

window对象给我们提供了一个location属性用于获取或设置窗体的URL,并且可以用于解析URL,因为这个属性返回的是一个对象,所以讲这个属性也称为location对象。

navigator对象

navigator 对象包含有关浏览器的信息,它有很多属性,我们最常用的是 userAgent,该属性可以返回由客户机发送服务器的 user-agent 头部的值。

Screen对象

Screen 对象包含有关用户屏幕的信息。

history对象

window对象给我们提供了一个 history对象,与浏览器历史记录进行交互。该对象包含用户(在浏览器窗口中)访问过的URL。

2. 函数的种类

截屏2023-09-23 下午8.09.30.png

3. this指向

  • 调用时决定,内部由JavaScript私有属性[[thisMode]]值决定

截屏2023-09-23 下午7.55.38.png

箭头函数

  • 指向定义时的上下文

4. 遍历对象属性

for in循环

  • 返回对象上所有可枚举的属性,包括原型上的属性

Object.keys()

  • 推荐使用返回对象上可枚举的属性,不包含原型上的

遍历所有属性方法

  • Object.getOwnPropertyNames()  方法返回一个由指定对象的所有自身属性的属性名(包括不可枚举属性但不包括 Symbol 值作为名称的属性)组成的数组。
  • Object.getOwnPropertySymbols()  方法返回一个给定对象自身的所有 Symbol 属性的数组。
  • Reflect.ownKeys 方法返回值等价于 Object.getOwnPropertyNames(target).concat(Object.getOwnPropertySymbols(target)) 。

5. 0.1 + 0.2 === 0.3 ?

背景

JavaScript 计算 0.1+0.2 会出现不等于0.3的情况,其实不只是 JavaScript 中会存在这个问题。只要是使用 IEEE 754 规范都会存在这个问题比如说 java 中也会存在这个问题。

IEEE 754

  • 在 IEEE 754 规范中双精度浮点数使用 64 进行存储的。
  • 对于64为双精度浮点数来说最高位为符号位 S 紧接着11位为指数位E最后剩余的52位为有效数字 M

有效数字

IEEE 754 规定在计算机内部存储有效数字 M 时,默认这个的第一位为1,此时会被舍去只保存后面部分。比如说计算机在保存 110.001 会只保存10001。这也是为什么我们在看二进制需要先找到第一个 1 出现的位置。

指数

IEEE 754 规定在计算机内部存储指数 E 时,在双精度浮点数中E 11 位取值范围为 [0,2047] 。为了表示指数 E 的正负所以必须减去1023。其中 [0,1022]表示负数指数,[0,1024]表示正指数。比如说 110.001 的指数为 2 ,保存时 1023+210000000001

  • 当指数位中 11 位都为 1 时,有效数字为0时表示无穷 Infinity ,结合符号位就有了正无穷 Infinity 与负无穷之分 -Infinity
  • 当指数位中 11 位都为 1 时,有效数字不为0时表示 NAN

为什么 0.1 + 0.2 !=== 0.3

0.1 + 0.2的计算过程

了解完十进制转换成二进制的过程我们对 0.10.2分别转换成为二进制,在JavaScript中我们可以通过toString(2)来得到二进制。

(0.1).toString(2)
// 0.0001100110011001100110011001100110011001100110011001101

(0.2).toString(2)
// 0.001100110011001100110011001100110011001100110011001101

将其转换为科学计数法

0.1 = 1.1001100110011001100110011001100110011001100110011010 * 2^-4
0.2 = 1.1001100110011001100110011001100110011001100110011010 * 2^-3

次方数不同不能进行计算我们需要将0.1的转为-3次方,这边需要注意由于指数从-4转化为-3,此时有效位数已经超出52位由于超出部分为0直接舍去。

0.1 = 0.1100110011001100110011001100110011001100110011001101 * 2^-3
0.2 = 1.1001100110011001100110011001100110011001100110011010 * 2^-3

计算0.1+0.2会发生进位

10.110011001100110011001100110011001100110011001100111 * 2^-3

将-3次方转换为-2次方,这时有效数字位数大于52且被截取部分为1那么需要往前进1,转化得

1.011001100110011001100110011001100110011001100110100 * 2^-2

在计算机内存中表示

0  1111111101  1011001100110011001100110011001100110011001100110100

解决方案

在我们实际工作中可以使用toPrecision方法来解决0.1 + 0.2 !=== 0.3

(0.1+0.2).toPrecision(12)
// 0.300000000000
Number((0.1+0.2).toPrecision(12)) === 0.3
// true

6. 防抖和节流

函数防抖关注一定时间连续触发的事件,只在最后执行一次; 而函数节流一段时间内只执行一次

  • 节流 在一段连续操作中,每一段时间只执行一次。
    • 例如:倒计时发送验证码,在一个单位时间内,只能触发一次
  • 防抖 在一段连续操作结束后,处理回调
    • 例如:搜索框输入防抖,用户输出完整的字符后,才会发出请求
// 节流
export default {
    methods: { 
        input: _.throttle(function(){ 
            console.log(this.value); 
        },1000) 
    }
}

// 防抖
export default {
    methods: { 
        input: _.debounce(function(){ 
            console.log(this.value); 
        },1000) 
    }
}

7. 如何获取 loaction 中参数值

// 获取 URL 中的查询参数
var queryString = window.location.search;

// 去除问号,并将查询参数转换为对象形式
var params = new URLSearchParams(queryString);

// 获取特定参数的值
var paramValue = params.get('paramName');

console.log(paramValue);
  • window.location.search 返回的是包含查询参数的字符串,包括问号;

  • 然后,我们使用 URLSearchParams 对象将查询参数转换为键值对的形式,方便我们进行操作;

  • 最后,使用 get 方法来获取特定参数的值。

8. 搞懂JS继承

概述

  • 「类式继承(原型链继承)」
  • 「构造函数继承」
  • 「组合继承」
  • 「原型式继承」
  • 「寄生式继承」
  • 「寄生组合式继承」

"new" 究竟发生了什么?

要了解继承,得先了解 new

  • new 实际上是把构造函数原型(prototype)上的属性放在了原型链(proto)上,那么当实例化对象取值时就会在原型链上取,而实例化对象上的 prototype 已经不见了

  • 可以简单理解为 new 实际上是将构造函数的 prototype 上的属性放在了实例化对象的__proto__上 ,通过实例化对象 . 属性名进行取值

那么 new 如何实现呢?

exports.newClass = function () {  
  const _target = new Object(); // 新增一个容器,用来装载构造函数(目标类)prototype上的所有属性  
  const _this = this//不能直接通过 this() 来运行构造函数,所以用一个变量装载  
  _target.__proto__ = _this.prototype// 核心部分:将构造函数prototype上的所有属性放到新容器中  
  const result = _this.apply(_target, arguments); // 执行构造函数,相当于执行class中的constructor  
  return result && typeof result === "object" ? result : _target; // 若函数返回值为引用类型返回当前函数执行结果,否则将新的容器返回,此时通过 _target[属性名]就可以访问 this.prototype 中的属性了  
};

综上所述,实例化一个构造函数,实际上可以简单理解为将类的 prototype 上的属性转移到实例化对象中

类式继承(原型链继承)

function classInheritance(SuperClass, SubClass) {  
  SubClass.prototype = new SuperClass();  
}  
function SuperClass(props) {  
  this.state = props;  
  this.info = { color"red" };  
}  
SuperClass.prototype = {  
  name"Car",  
};  
classInheritance(SuperClassSubClass);  
function SubClass() {  
  this.price = 1000;  
}  
const BMW = new SubClass();  
const BenZ = new SubClass();  
console.log(BMWBMW.__proto__BMW.__proto__.__proto__); // { price: 1000 } { state: undefined, info: { color: 'red' } } { name: 'Car' }  
console.log(BenZ.nameBenZ.info); // Car { color: 'red' }

类式继承实际上是

  1. 通过 new 将 SuperClass.prototype 绑定到 SuperClass.__proto__上
  2. 然后赋值给 SubClass.prototype,当实例化 SubClass 时,SubClass.__proto__上也会带有 SuperClass 及其原型链上的属性

优点:

  • 子类拥有父类及父类 prototype 上属性

缺点:

  • 子类通过 prototype 继承父类,只能父类单向传递属性给子类,无法向父类传递参数
  • 继承的引用类型属性都有引用关系, 即当父类原型上的引用属性改变时,所有子类实例相对应的引用属性都会对应改变
  • 子类只能继承一个父类

构造函数继承

function SuperClass(props) {  
  this.state = props;  
  this.info = { color"red" };  
}  
SuperClass.prototype = {  
  name"Car",  
};  
function SuperClass2() {  
  this.size = 'small';  
}  
// 注意:构造函数使用 call 会重写子类同名属性,要写在子类的最开始  
function SubClass() {  
  SuperClass.call(this, ...arguments);  
  SuperClass2.call(this, ...arguments);  
  this.price = 1000;  
}  
SubClass.prototype.name = "Small Car";  
const BMW = new SubClass(true);  
const BenZ = new SubClass(false);  
console.log(BMWBMW.__proto__BMW.__proto__.__proto__);  
// SubClass {  
//   state: true,  
//   info: { color: 'red' },  
//   size: 'small',  
//   price: 1000  
// } SubClass { name: 'Small Car' } {}  
console.log(BenZ.nameBenZ.infoBenZ.state); // Small Car { color: 'red' } false

构造函数继承实际上是

  • 在 SubClass 构造函数中使用 SuperClass.call 直接运行 SuperClass 构造函数

优点:

  • 可以在 SuperClass 执行时传参数
  • 可以继承多个父类
  • 继承同一个父类的子类的属性之间不会有引用关

缺点:

  • 父类 prototype 上的属性无法继承,只能继承父类构造函数的属性,父类的函数无法复用
  • 父类 SuperClass 每次在子类 SubClass 中执行都会在每个子类重新初始化 this.属性或 this.函数,这些属性是属于每个子类单独的,这样既增加了性能负担又使父类原型中的公共属性无法复用

组合继承

function classInheritance(superClass, subClass) {  
  subClass.prototype = new superClass();  
}  
let count = 0;  
function SuperClass(props) {  
  this.state = props;  
  this.info = { color"red" };  
  console.log(++count);// 打印 1 2 3  
}  
SuperClass.prototype = {  
  name"Car",  
};  
classInheritance(SuperClassSubClass);  
function SubClass() {  
  SuperClass.call(this, ...arguments);  
  this.price = 1000;  
}  
SubClass.prototype.name = "Small Car";  
const BMW = new SubClass(true);  
const BenZ = new SubClass(false);  
  
console.log(BMWBMW.__proto__BMW.__proto__.__proto__);  
// { state: true, info: { color: 'red' }, price: 1000 } { state: undefined, info: { color: 'red' }, name: 'Small Car' } { name: 'Car' }  
console.log(BenZ.nameBenZ.infoBenZ.state);// Small Car { color: 'red' } false

优点:

  • 解决类式继承和构造函数继承的主要问题:
    • 构造函数继承不能继承父类原型上的属性,而类式继承无法传参给父类

缺点:

  • 父类构造函数执行两遍,性能损耗

原型式继承

constsubClass=Object.create(superClass)

优点:

  • 无子类构造函数开销,相当于实现了对象的浅复制

缺点:

  • 继承时无法向父类传参
  • 和类式继承一样,继承父类的引用类型属性都有引用关系

寄生式继承

function prototypeInheritance(superClass) {  
  function F() {}  
  F.prototype = superClass;  
  return new F();  
}  
function SuperClass(props) {  
  this.state = props;  
  this.info = { color"red" };  
}  
SuperClass.prototype = {  
  name"Car",  
};  
  
function parasiticInheritance(superClass) {  
  const subClass = prototypeInheritance(superClass);  
  subClass.type = { electricitytruegasolinefalse };  
  return subClass;  
}  
  
const superClass = new SuperClass(true);  
const BenZ = parasiticInheritance(superClass);  
const BMW = parasiticInheritance(superClass);  
console.log(BenZ.type === BMW.type); // false  说明每个子类的属性都不一样  
console.log(BenZBenZ.__proto__BenZ.__proto__.__proto__);  
// { type: { electricity: true, gasoline: false } } { state: true, info: { color: 'red' } } { name: 'Car' }  
console.log(BMWBMW.nameBMW.infoBMW.state); // { type: { electricity: true, gasoline: false } } Car { color: 'red' } true

优点:

  • 无子类构造函数开销
  • 继承父类所有属性
  • 子类拥有自己的属性

缺点:

  • 继承时无法向父类传参
  • 和类式继承一样,继承父类的引用类型属性都有引用关系
  • 子类公共属性无法在原型上定义,导致无法复用

寄生组合式继承


function parasiticCombinatorialInheritance(SuperClass, SubClass) {
  SubClass.prototype = Object.create(SuperClass.prototype);
  SubClass.prototype.superClass = SuperClass;
}

function SuperClass(props) {
  this.state = props;
  this.info = { color"red" };
}
SuperClass.prototype = {
  name"Car",
};

function SubClass() {
  this.superClass.call(this, ...arguments); //调用一下父类构造函数,将父类的属性放在子类中
}

parasiticCombinatorialInheritance(SuperClassSubClass);
SubClass.prototype.name = "small car";//修改prototype值写在继承后面
const BMW = new SubClass(true);
const BenZ = new SubClass(false);
console.log(BenZBenZ.__proto__BenZ.__proto__.__proto__); // { state: false, info: { color: 'red' } } { superClass: [Function: SuperClass], name: 'small car' } { name: 'Car' }
console.log(BMW.info); // { color: 'red' }
BenZ.info.color = "blue";
console.log(BenZ.name,BenZ.info); // small car { color: 'blue' }
console.log(BenZ.name,BMW.info); // small car { color: 'red' }
console.log(BMW instanceof SuperClass); // true
console.log(BenZ instanceof SuperClass); // true

解决了组合式继承的父类构造函数调用两次的问题,只创建了一次父类属性,并且子类拥有父类原型上的属性