JS基础(二)

目录

面向对象

计算机程序是对真实世界的抽象映射,程序世界对应着真实世界,程序员的目标就是把真实世界中的一部分映射到程序世界
现实中每个事物都有自己的特性和行为,那么在JS中,每个事物都可以定义为一个对象,对象有属性和方法,属性是对象的数据,方法是对象的行为。JS这门语言就是面向对象编程,和java一样。(OOP: Object Oriented Programming)

对象是类的实例,类是对象的模板,通过类可以创建许多对象,这些对象就是同一类对象

定义类语法: class 类名{}const 类型 = class {}
类名要用大驼峰命名,例如, Persion,Student

使用类创建实例对象语法: new 类名()

class Person { }
let personOne = new Person();
let personTwo = new Person();
console.log(personOne, personTwo); // 输出: Person {} Person {}

使用instanceof判断对象是否为某个类的实例

console.log(personOne instanceof Person); // 输出: true

类的属性

类的代码块中默认是严格模式

静态属性

在类中不能使用关键字声明属性,只能定义属性名和值,使用static关键的属性为类属性,也称为静态属性,只能通过类访问,不能通过实例访问

class Person {
    name;
    age;
    height = 100;
    static sex;
    static hobby = '打游戏';
}
let zhangsan = new Person();
console.log(zhangsan); // 输出: Person {name: undefined, age: undefined, height: 100}
console.log(Person.sex, Person.hobby, zhangsan.hobby); // 输出: undefined '打游戏' undefined

不管实例属性还是类属性都可以设置默认值

注意:
1.静态属性只能通过static关键字声明
2.控制台打印出实例对象中,静态属性会隐藏
3.只能通过类访问静态变量

实例属性

只能通过类的实例对象访问的属性 属性前面没有static

let zhangsan = new Person();
let age = zhangsan.age;

注意:
实例属性可先不定义,直接在构造函数中定义

class Person2 {
    constructor(name) {
        this.name = name;
    }
}
let zhangsan1 = new Person2('张三');
console.log(zhangsan1.name); // 输出:张三

类属性

只能通过类访问的属性,属性前面有static

let hobby = Person.hobby;

私有属性

在属性前面紧贴着添加#号,就表示该属性为私有属性,私有属性只能在类内部访问,这和java相同,java的private成员变量在同类的实例也不可以访问,但可以通过提供public get方法获取私有属性值

class Person {
    #age;
    constructor(age) {
        this.#age = age;
    }
    fn() {
        console.log(this.#age);
    }
}
const zhangsan0 = new Person(2);
zhangsan0.fn(); // 输出: 2
console.log(zhangsan0); // 输出: Person {#age: 2}
console.log(zhangsan0.#age); // 输出错误: Private field '#age' must be declared in an enclosing class

注意

1.私有属性只能在类中访问,类的实例也不能访问,否则会报错
2.私有属性需要先定义,不能在构造函数中定义,而实例属性可以不定义,因为在构造方法中也能定义
3.控制台打印出实例对象中,私有属性不会隐藏

作用:
1.对属性值进行保护,控制修改读取的权限,如,可以提供set方法,或get方法
2.只暴露一个set方法,通过set方法可以校验属性值的合法性

类的方法

类的构造函数

类的构造函数语法: constructor(参数1,参数2){}
执行时机: 在创建实例对象时执行
构造函数中的this就是当前创建的实例对象

class Person {
    name = '张三';
    constructor(name) {
        this.name = name;
    }
}
const zhangsan = new Person('李四');
console.log(zhangsan); // 输出:Person { name: '李四' }

静态方法

class Person {
    name = '张三';
    fn() {
        console.log(this);
    }
    static fn2() {
        console.log(this);
    }
    fn3 = () => {
        console.log(this);
    }
}
let zhangsan = new Person();
zhangsan.fn(); // 输出: 实例对象shangsan Person { name: '张三' }
Person.fn2(); // 输出: 类 class Person {}
zhangsan.fn3(); // 输出: 实例对象shangsan Person { name: '张三' }

类中静态方法中的this就是类本身(类的构造),而非静态方法中的this就是类的实例对象
img

class Person {
    name = '张三';
    static fn2() {
        console.log(this);
    }
}
let zhangsan = new Person();
console.log(zhangsan); // 输出: Person { name: '张三' }

注意:
1.控制台打印对象实例中,不显示静态方法
2.只能通过类访问静态方法


类的get方法

指定get方法: get 属性名(){} 使用关键字get指定一个方法为get方法,允许被多个get方法覆盖
使用get方法: 对象.属性名,就像读取对象实例变量一样

class Person {
    #age;
    constructor(age) {
        this.#age = age;
    }
    get age() {
        return this.#age;
    }
    get age() {
        return this.#age;
    }
}
const zhangsan0 = new Person(2);
console.log(zhangsan0.age); // 输出:2

类的set方法

指定set方法: set 属性名(属性名){} 使用关键字set指定一个方法为set方法,允许被多个set方法覆盖
使用set方法: 对象.属性名 = 属性值,就像赋值对象实例变量一样

class Person {
    #age;
    constructor(age) {
        this.#age = age;
    }
    set age(age) {
        this.#age = age;
    }
    get age() {
        return this.#age;
    }
}
const zhangsan0 = new Person(2);
zhangsan0.age = 4;
console.log(zhangsan0.age); // 输出:4

多态

java中的多态: 父类引用指向子类对象
而js中的多态,更像是函数的形参可以指向不同类的实例

class Skill {
    show() {
        throw new Error('抽象方法,无具体实现');
    }
}

class Person extends Skill {
    show() {
        console.log('喊你吃饭');
    }
}
class Dog extends Skill {
    show() {
        console.log('汪汪汪');
    }
}
function showSkill(skill) {
    skill.show();
}
const dog = new Dog();
const person = new Person();
showSkill(person); // 输出:喊你吃饭
showSkill(dog); // 输出:汪汪汪

继承

使用extends关键字可以继承父类中的所有属性和方法以及构造函数

class Skill {
    name = '技能';
    show() {
        console.log('哈哈');
    }
}
class Person extends Skill {}
class Dog extends Skill {}
const dog = new Dog();
const person = new Person();
dog.show(); // 输出:哈哈
person.show(); // 输出:哈哈
console.log(dog.name); // 输出:技能

派生类继承了父类时,其构造函数中,必须先调用super方法

class Skill {
    name = '技能';
    constructor(name) {
        this.name = name;
    }
    show() {
        console.log('哈哈');
    }
}
class Person extends Skill {
    constructor(name) {
        // super(name);
    }
}
const person = new Person();
person.show();

派生类重写构造时,需要先调用super方法,否则报错
Must call super constructor in derived class before accessing 'this' or returning from derived constructor: 在派生类中访问“this”或从派生类构造函数返回之前,必须调用基类构造函数


class Person extends Skill {
    constructor(name) {
        super(name); // Skill.call(this, name)
    }
    show() {
        super.show(); // 输出: 哈哈
    }
}
const person = new Person();
person.show();

super方法的作用:
1.调用父类的构造,创建父类实例,创建子类对象时才能继承父类实例
2.子类方法中调用父类的方法,super.父类方法(),其实是调用了父类的call方法

三个特性

封装: 提高类中变量数据安全性


继承: 提高代码复用性,和扩展性
OCP(Open-Closed 2 )开闭原则: 软件实体(类、模块、函数等)应该对扩展开放,对修改关闭


多态: 提高代码灵活性,不会对形参进行类型校验


对象结构

class Person {
    age = 10;
    name = '孙悟空';
    test = () => { }
    sayHello2 = function () { }
    sayHello(){}
}
const person = new Person();
console.log(person); // Person {age: 10, name: '孙悟空', test: ƒ, sayHello2: ƒ}

上面代码中,打印出实例对象的成员变量和方法,但是sayHello()方法没有打印出来

对象中的数据存储位置有两个
1.对象本身
x = y形式定义的数据格式都会存储在对象中
2.不以x = y形式定义的数据存储在prototype对象(即原型对象)中
img

Person对象中包含__proto__属性,存储原型对象的地址
img

原型对象与自身对象中数据访问优先级: 自身对象 > 原型对象
当自身对象中找不到时,会从原型对象中寻找

class Person {
    age = 10;
    name = '孙悟空';
    sayHello() {
        console.log("prototype中的方法");
    }
    sayHello = function () {
        console.log("对象中的方法");
    }
}
const person = new Person();
console.log(person.sayHello()); // 输出: 对象中的方法

哪些数据会添加到原型对象中

1.对象中以方法形式定义的方法,例如方法名(){},会添加到原型对象中,箭头形式和表达式形式方法不会添加到原型对象中
2.主动添加到原型对象中的数据
3.构造方法


原型对象

在 JavaScript 中,原型对象(Prototype Object) 是实现继承和共享属性的核心机制。它是每个对象(或函数)在创建时自动关联的一个特殊对象,用于定义其他对象可继承的属性和方法

原型对象的结构

prototype(显示原型对象属性)(仅函数/类拥有):每个函数(包括构造函数、ES6 的类)都有一个 prototype 属性,指向该函数的原型对象。包含了对象的数据和方法以及构造函数
__proto__(隐式原型对象属性)(所有对象拥有):每个对象都有一个__proto__属性(非标准,但被浏览器实现),指向其构造函数的原型对象(即 构造函数.prototype)。
constructor属性:每个原型对象都有一个 constructor 属性,指回其关联的构造函数。

img


对象的__proto__指向其构造的prototype属性

img

继承关系的原型属性

class Person {
    age = 25;
}
class Teacher extends Person {
    constructor() {
        super();
    }
}
class Student extends Teacher {
    constructor() {
        super();
    }
}
let person = new Person();
// 子类构造函数的原型的原型就是父类的原型
console.log(Student.prototype.__proto__ === Teacher.prototype); // true
// 实例对象的隐式原型就是其构造函数的显示原型
console.log(person.__proto__ === Person.prototype); // true
// 实例对象的构造的原型就是类的原型
console.log(person.constructor.prototype === Person.prototype); // true

prototype__proto__constructor关系

let obj = {}
class Person {
    sayHello() {
        console.log("prototype中的方法");
    }
}
let person = new Person();
console.log('obj.prototype', obj.prototype); // 输出: undefined
console.log('obj.__proto__', obj.__proto__); // 输出: Object的原型对象
console.log('obj.constructor', obj.constructor); // 输出: Object的构造函数
console.log('Person.prototype', Person.prototype); // 输出: Person类的原型对象
console.log('Person.__proto__', Person.__proto__); // 输出: ƒ () { [native code] }
console.log('Person.constructor', Person.constructor); // 输出: function Function() { [native code] }
console.log('person.prototype', person.prototype); // 输出: undefined
console.log('person.__proto__', person.__proto__); // 输出: person的原型对象
console.log('person.constructor', person.constructor); // 输出: Person类
console.log('Person', Person); // 输出: Person类
console.log('obj.__proto__ === obj.constructor.prototype', obj.__proto__ === obj.constructor.prototype); // 输出: true
console.log('person.constructor === person.__proto__.constructor', person.constructor === person.__proto__.constructor); // 输出: true
console.log('Function.prototype', Function.prototype); // 输出: ƒ () { [native code] }

obj.prototype: 普通对象(非函数对象)没有 prototype 属性。只有函数(或类)才有 prototype 属性。obj 是通过字面量 {} 创建的空对象,因此它的 prototype 为 undefined
img


obj.__proto__: 对象的__proto__指向其构造函数的原型。obj 的构造函数是 Object,因此 obj.__proto__ 指向 Object.prototype
结构:Object.prototype 包含默认方法(如 toString、hasOwnProperty 等)
img


obj.constructor: obj.constructor继承自Object.prototype.constructor,指向 Object 构造函数。
img
console.log('Object->', Object);: Object类的就是Object构造函数
img


Person.prototype: 类的prototype属性指向其原型对象,原型对象中存储类的方法(如 sayHello)
img


Person.__proto__: Person 是类(本质是函数),函数的__proto__指向 Function.prototype。
img


Person.constructor: Person.constructor 继承自 Function.prototype.constructor,指向 Function 构造函数(因为 Person 是函数)
img


person.prototype: 实例对象没有 prototype 属性,只有函数/类才有该属性。
img


person.__proto__: 实例的__proto__指向其构造函数的原型(即 Person.prototype)。
结构:与 Person.prototype 一致,包含 sayHello 方法。
img


person.constructor: person.constructor 继承自 Person.prototype.constructor,指向 Person 类。
img


Person: Person类
img


obj.__proto__ === obj.constructor.prototype true: 对象的原型指向其构造函数的原型
person.constructor === person.__proto__.constructor true: 对象的构造就是对象原型的构造,本质就是类
Function.prototype: 函数的原型对象
img

console.log(Person.__proto__); // 输出: ƒ () { [native code] } Function.prototype
console.log(person.constructor === Person); // 输出: true 对象的构造就是类
console.log(person.constructor === Person.prototype.constructor); // 输出: true 对象的构造就是类,也是类的原型对象的构造


同类型的对象它们的原型对象相同

class Person {
}
const person = new Person();
const person2 = new Person();
console.log(person.__proto__ === person2.__proto__); // 输出: true

访问一个对象中的原型对象的方式

1.对象的__proto__属性,可通过对象.__proto__来访问一个
2.Object.getPrototypeOf(对象)

class Person {
    age = 10;
    name = '孙悟空';
    sayHello() {
        console.log("prototype中的方法");
    }
    sayHello = function () {
        console.log("对象中的方法");
    }
}
const person = new Person();
console.log(person);
console.log(person.constructor);
console.log(person.__proto__);
console.log(Object.getPrototypeOf(person));
console.log(new person.constructor);
console.log(person.constructor === Person); // true

img
img

一个对象是有默认的构造方法的,并且该构造方法是作为该对象的原型对象的属性存储的,其值是该类
person.constructor: 访问的是原型对象的constructor属性,该属性的值是类,并不是类的实例
person.constructor === Person: true 因此创建一个类的实例也可以通过new 对象.constructor其实就是new Person()

原型链

class Person {
    sayHello() {
        console.log("prototype中的方法");
    }
    
}
const person = new Person();
const obj = {};
console.log(person.__proto__.__proto__.__proto__); // 输出: null
console.log(obj.__proto__.__proto__); // 输出: null

原型对象也有原型对象,这样就构成了一条原型链,根据对象的复杂程度不同,该对象的原型链长度不同
例如,person.__proto__.__proto__.__proto__输出null后就没有原型对象了
obj.__proto__.__proto__输出null后就没有原型对象了

原型链工作原理

在读取对象属性时,如果对象中没有,则去对象的原型对象中寻找,如果还是没有,则继续在原型对象的原型对象中寻找,一直找到没有原型对象的原型对象,那么该对象就是Object对象,为最底层对象,因为Object对象没有原型对象,因此Object的原型对象为null,如果Object中也没有该属性,就返回undefined

1.对象自身属性:先检查对象本身是否直接定义了该属性。
2.原型对象:若找不到,则通过 __proto__ 查找其构造函数的原型对象(构造函数.prototype)。
3.原型链递归:若仍找不到,继续沿着原型链向上查找(原型对象.__proto__),直到 Object.prototype(所有对象的顶层原型,其 __proto__ 为 null)。仍然找不到返回undefined


class Person {
    age = 25;
}
class Teacher extends Person {
    constructor() {
        super();
    }
}
class Student extends Teacher {
    constructor() {
        super();
    }
}
const student = new Student();
const person = new Person();
console.log(student);
console.log(Student.prototype);
console.log(Teacher.prototype);
console.log(Person.prototype);
console.log(Object.prototype);
console.log(student.__proto__ === Student.prototype); // true
console.log(student.__proto__.__proto__ === Teacher.prototype); // true
console.log(student.__proto__.__proto__.__proto__ === Person.prototype); // true
console.log(student.__proto__.__proto__.__proto__.__proto__ === Object.prototype); // true
console.log(student.__proto__.__proto__.__proto__.__proto__.__proto__ === null); // true

img
img

子类的原型对象其实就是祖父类的一个实例

console.log(Student.prototype instanceof Student); // false 
console.log(Student.prototype instanceof Person); // true

总结

1.prototype: 只有函数/类才有,用于定义实例共享的属性和方法。
2.__proto__: 所有对象都有,指向构造函数的原型(形成原型链)。
3.constructor: 对象的构造就是对象所属类。对象继承了对象对应类的原型的构造,即类本身
4.同类型的对象它们的原型对象相同
5.子类的原型对象其实就是祖父类的一个实例


原型链和作用域链区别

原型链: 访问对象属性的链,找不到返回undefined
作用域链: 访问变量的链,找不到会报错,例如: Uncaught ReferenceError: 变量 is not defined


原型对象作用

1.共享属性和方法
原型对象存储了构造函数(或类)定义的公共方法和属性。所有通过该构造函数创建的实例对象,都会通过原型链继承这些属性和方法,避免重复占用内存。
2.实现继承
类的继承通过原型对象机制实现。通过原型链(__proto__ 的链式引用),对象可以访问其原型对象、原型的原型等层级的属性和方法,形成多级继承关系。

Person类继承了Animal的属性和方法,其实继承了Animal的原型对象

class Animal {
    age = 1
    sound() {
        console.log('动物发出声音');
    }
}
class Person extends Animal {
}
const animal = new Animal();
const person = new Person();
console.log(animal.__proto__.sound()); // 输出: 动物发出声音
console.log(person.__proto__.sound()); // 输出: 动物发出声音

person实例的原型的原型,就是Animal的原型对象,通过原型链找到祖先属性和方法

console.log(person.__proto__.__proto__ === Animal.prototype); // true
console.log(Animal.prototype); 

img


修改对象的原型对象

通过类的实例对象修改原型对象数据,那么所有的类的实例也就共享了该数据
Person类的另一个实例person2也可以访问test方法

class Person { }
const person = new Person();
person.__proto__.test = () => {
    console.log('类的原型对象中添加test方法');
}
const person2 = new Person();
person2.test(); // 输出: 类的原型对象中添加test方法

修改实例的原型属性指向后,不影响其他类的实例的原型属性

class Person { }
class Dog { }
const person = new Person();
person.__proto__ = new Dog();
const person2 = new Person();
console.log('person.__proto__', person.__proto__);
console.log('person2.__proto__', person2.__proto__);

修改实例的原型__proto__属性值,实际上是修改的原型属性的引用地址,不会修改所属类的原型对象,其他实例的原型对象仍然是所属类的原型
img

因此在修改类的原型对象数据时,不能通过类的实例修改,应该通过修改类的prototype属性达到修改类的原型对象的目的

class Person { }
class Dog { }
Person.prototype.test = () => {
    console.log('通过类的prototype属性修改原型对象数据');
}
const person = new Person();
console.log('person.test()', person.test()); // 输出: 通过类的prototype属性修改原型对象数据

优点:简单直观,不影响已有实例。
缺点:可能覆盖同名属性,需注意命名冲突。


instanceof和hasOwn

instanceof

instanceof关键字: 检测构造函数的prototype属性值是否出现在某个实例对象的原型链上

class Animal { }
class Dog extends Animal { }
const dog = new Dog();
console.log('dog instanceof Dog', dog instanceof Dog); // 输出: true
console.log('dog instanceof Animal', dog instanceof Animal); // 输出: true

作用:
1.判断对象类型:例如区分数组和普通对象。

const arr = [1, 2, 3];
console.log(arr instanceof Array);    // true

2.验证对象是否属于某个类

hasOwn

hasOwn: Object类的静态方法,如果指定的对象自身有指定的属性,则静态方法 Object.hasOwn() 返回 true。如果属性是原型对象中或者不存在,该方法返回 false。
语法: Object.hasOwn(对象实例, '属性名')

 class Animal {
    a;
    test = function () { }
    test1 = () =>{}
}
class Dog extends Animal {
    b;
    test2 = function () { }
    test3() { }
    test4 = () =>{}
}
const dog = new Dog();
console.log('a' in dog); // true
console.log('test' in dog); // true 
console.log(Object.hasOwn(dog, 'a')); // true
console.log(Object.hasOwn(dog, 'a')); // true
console.log(Object.hasOwn(dog, 'b')); // true
console.log(Object.hasOwn(dog, 'test')); // true
console.log(Object.hasOwn(dog, 'test2')); // true
console.log(Object.hasOwn(dog, 'test3')); // false
console.log(Object.hasOwn(dog, 'test4')); // true
console.log(Object.hasOwn(dog, 'test1')); // true

注意

1.new Dog(): 由父类构造函数直接添加到实例,因此在new实例时,父类中以键值对形式的属性也会变成实例的自身属性,键值对形式包含:
属性名
属性名 = 属性值
方法名 = function () {}
方法名 = () => {}
2.只有属性;x = ...,也包含箭头函数,都是以键值对写法,会变成自身的成员,method(){}形式不会添加到自身实例中,添加到原型对象中的属性
因此dog的test2和test4是自身属性,test3不是自身属性返回false

hasOwn和in的区别

in: 检查属性是否在自身对象和原型中,是继承过来的属性也会返回true
hasOwn: 不检查对象的原型链中的指定属性,自身对象没有,但是原型中有仍然返回false

hasOwn和hasOwnProperty区别

hasOwnProperty属于原型对象的方法,但是null对象没有原型对象在使用该方法时,会报错undefined,因此官方重写了hasOwnProperty方法,并推荐使用hasOwn
img


旧类

ES6前定义一个类的语法function 类名(){}其实就是一个函数,旧类不能添加method(){}形式的方法,也就不能自动添加到原型对象中
img

function Animal(name) {
    this.name = name;
}
Animal.prototype.test = () => { }
const animal = new Animal();
console.log(animal.__proto__);

旧类通过new 函数名()创建类的实例,通过类的prototype显示添加原型对象的方法
img

// 添加静态方法
Animal.staticMethod = ()=>{console.log('静态方法')}
// 添加静态属性
Animal.staticProper = 123
console.log(Animal.staticMethod()) // 输出: 静态方法

定义属性和方法比较分散,为避免代码书写繁杂和混乱,旧类的定义放在立即执行函数中,并返回函数名

var Animal = (
    function () {
        function Animal(name) {
            this.name = name;
        }
        Animal.prototype.test = () => { }
        // 添加静态方法
        Animal.staticMethod = () => { console.log('静态方法'); }
        // 添加静态属性
        Animal.staticProper = 123
        return Animal
    }
)()
const animal = new Animal();
console.log(animal.__proto__);
console.log(Animal.staticMethod()); // 输出: 静态方法

继承父类写法,函数名.prototype = new 函数名()

const animal = new Animal();
console.log(animal.__proto__);
console.log(Animal.staticMethod()); // 输出: 静态方法
// 继承
var Cat = (function () {
    function Cat() { }
    Cat.prototype = new Animal()
    return Cat
})()
const cat = new Cat()
console.log(cat);

img


new运算符

当使用 new 关键字调用函数时,该函数将被用作构造函数

调用构造函数的操作

1.创建一个空的简单 JavaScript 对象。我们称之为 新的实例

const animal = new Animal();
console.log(Animal.prototype); // 输出: {} 是一个Object的实例对象

2.如果构造函数的 prototype 属性是一个对象,则将 newInstance 的 [[Prototype]] 指向构造函数的 prototype 属性,否则 newInstance 将保持为一个普通对象,其 [[Prototype]] 为 Object.prototype。
将构造函数的prototype属性值赋值给实例的原型对象,因此实例的原型对象就是类的prototype属性值

console.log(animal.__proto__ === Animal.prototype); // 输出: true

3.使用给定参数执行构造函数,并将 新实例 绑定为 this 的上下文(换句话说,在构造函数中的所有 this 引用都指向 新实例)
4.如果构造函数返回非原始值,则该返回值成为整个 new 表达式的结果。否则,如果构造函数未返回任何值或返回了一个原始值,则返回 newInstance。(通常构造函数不返回值,但可以选择返回值,以覆盖正常的对象创建过程。)

为什么new 类()会直接把父类的成员添加到自身实例中

class Animal {
    a = 1
    test = () => { }
}
class Dog extends Animal {
    b = 2
    constructor() {
        super();
    }
}
const dog = new Dog()
console.log(dog); // 输出: Dog {a: 1, b: 2, test: ƒ}

执行过程:
1.创建新对象
2.执行子类构造函数 Dog,调用 Dog 的 constructor,此时 this 指向新创建的对象。
3.执行父类构造函数 Animal,super() 会触发父类 Animal 的构造函数逻辑。
super()的作用:
(1)用于执行父类构造函数逻辑,必须确保父类初始化完成后再初始化子类,引擎将子类实例(this)隐式传递给父类,完成父类定义的实例属性初始化
(2)建立原型链继承关系(Dog.prototype 继承 Animal.prototype)

初始化实例属性:父类中通过类字段语法(如 a = 1 或 test = () => {})定义的属性,会被直接挂载到 this(即当前实例 dog)上。
因为new Dog()调用了Dog的构造,并且this指向new Dog(),因此父类构造逻辑中完成了dog新实例的成员赋值
这相当于在父类构造函数中执行了:

constructor() {
    console.log(this); // 这里的this指向new Dog(),
    this.a = 1;
    this.test = () => { }
}

4.完成子类构造函数
父类构造函数执行完毕后,子类 Dog 的构造函数继续执行。
若子类有额外的实例属性(例如 b = 2),也会在此阶段挂载到 this 2
5.返回实例


对象总结

JS中对象分类:
1.内置对象: 由ES标准定义的对象
Object、Function、String、Number...

2.宿主对象: js语言运行在所在的环境中
浏览器环境中的BOM中的对象、DOM的对象,console、document、window等
Node.js环境中提供的对象

3.开发者自定义的业务对象


数组

创建数组语法:
1.[元素1,元素2...]: 字面量方式直接指定元素值
2.new Array(4): 表示创建一个长度为4的空数组
3.new Array(元素1,元素2): 表示创建一个包含元素1和元素2的数组
4.Array(4)Array(元素1,元素2): new运算符可以省略

let arr = []
let arr2 = new Array(3)
let arr3 = Array(1,2,3)
console.log(arr, arr2,arr3);

img


新增/修改元素
通过索引值添加/修改语法: 数组[index] = 元素

arr[1] = 1

存在问题:
1.容易造成数组数据不连续,例如arr[1] = 1,arr[100] = 100,那么索引2-99之间没有对应元素,导致资源浪费
2.性能不好,因为要补充索引之间的空元素

访问数组语法: 数组[index],如果读取了一个不存在的索引,不会报错,会返回undefined

特点

1.存储的数据类型可以不同
img
2.存储的数据可以重复
3.有序存储

使用示例:

let arr = [0,3];
let num = arr[1]++;
console.log(num);
console.log(arr);

let num = arr[1]++,先赋值给num,然后再对arr[1]的值加1
img

length属性

返回数组元素个数

技巧

arr.length = 1 // 删除脚标大于0的元素
arr.length = 10 // 补充空元素直到脚标为9

img


for-of和for-in遍历数组

let arr = [1, 2, 3];
for (const element of arr) {
    console.log(element);
}
for (const element in arr) {
    console.log(arr[element]);
}

for-in遍历的时数组的脚标
img


数组中的方法

slice()方法: 分隔数组返回新数组,对原数组无影响

let arr = [0,3];
let newarr = arr.slice(1,2);
console.log('arr:', arr);
console.log('newarr:', newarr);

arr.slice(): 以浅拷贝方式返回新数组, 不会改变原数组
arr.slice(start,end): start: 开始索引(包含)闭区间,end: 结束索引(不包含)开区间
截取字符串区间: [start, end),包含start,不包含end
img
arr.slice(索引): 从索引值开始截取后面所有的数组元素,返回新数组

let arr6 = [1, 2, 3, 4];
console.log(arr6.slice(1)); // 输出: (3) [2, 3, 4]

Array.isArray(数组):判断对象是否是一个数组

let arr = [1, 2, 3];
console.log(Array.isArray(arr)); // 输出: true

split(字符串):字符串转数组

let str = '1';
console.log(str.split(',')); // 输出: ['1']
let str2 = '1,2';
console.log(str2.split(',')); // 输出: (2) ['1', '2']
let str3 = ',2';
console.log(str3.split(',')); // 输出: (2) ['', '2']
let str4 = '112211';
console.log(str4.split('11')); // 输出: (3) ['', '22', '']

注意

1.字符之前或之后即时没有字符,也会分割成一个空元素
2.如果字符串中没有匹配到对应的字符,会把整个字符串作为一元素


join(字符串): 将数组中元素以某个字符串拼接返回字符串

join(): 当不指定参数时,默认以逗号拼接

let obj = { name: 'obj' };
let arr0 = [1];
let arr4 = [1, { name: '张三' }, [1, {}], function a() { }, new Number(3), obj, arr0];
let arr5 = [];
console.log(arr4.join('$$')); // 输出: 1$$[object Object]$$1,[object Object]$$function a() { }$$3$$[object Object]$$1
console.log(arr5.join('$$')); // 输出: '' 空串

注意

使用join连接数组元素,当数组中的元素是字面对象时返回转为[object Object],当元素是数组时,也会拼接数组中的元素内容


Array.of(参数): 创建一个数组

let arr2 = Array.of({},1);
console.log(arr2); // 输出: (2) [{…}, 1]

at(整数): 获取指定脚标元素

let arr2 = Array.of({}, 1);
console.log(arr2); // 输出: (2) [{…}, 1]
console.log(arr2.at(1)); // 输出: 1
console.log(arr2.at(10)); // 输出: undefined
console.log(arr2.at(-2)); // 输出: {}

at(正整数): 获取指定脚标的元素
at(负整数): 从数组中倒数获取指定位数的元素,at(-2),倒数第二个元素


concat(数组1,数组2): 将多个数组拼接成一个新数组,不会改变原数组

console.log(arr.concat(arr2)); // 输出: (5) [1, 2, 3, {…}, 1]


indexOf(元素,索引): 从指定索引位置开始查找元素第一次出现所在的索引值

let arr3 = [1,1,2,3,3];
console.log(arr3.indexOf(1)); // 输出: 0
console.log(arr3.indexOf(1,0)); // 输出: 0
console.log(arr3.indexOf(3,4)); // 输出: 4

arr3.indexOf(1): 默认从索引值0开始往后查找,第一次出现1的索引值
arr3.indexOf(3,4): 从索引值4开始往后查找,第一次出现3的索引值,因此返回4


lastIndexOf(元素,索引): 从指定索引位置开始从后向前查找元素,第一次出现所在的索引值

console.log(arr3.lastIndexOf(3, 4)); // 输出: 4

从索引值为4开始从后向前查找3第一次出现的位置索引

indexOf、lastIndexOf如果没有找到对应的元素则返回-1,用于判断元素是否存在


includes(): 检测数组中是否包含一个元素

let arr = [100, 20, 50, 58, 6, 69, 36, 45, 78, 66, 45, NaN];
let flag = arr.includes(1100);
let flag1 = arr.includes(NaN);
console.log(flag, flag1); // false true

数组的其他方法(改变原数组)

push(): 向一个数组中追加一个或多个元素,并返回数组操作后数组长度

let arr = [1, 2, 3];
let length = arr.push(4,5);
console.log(arr); // 输出: (5) [1, 2, 3, 4, 5]
console.log(length); // 输出: 5

pop(): 删除最后一个元素,并返回删除的元素

let arr2 = [1, 2, 3];
let result = arr2.pop();
console.log(arr2); // 输出: (4) [1, 2]
console.log(result); // 输出: 3

unshift(): 向一个数组前面添加一个或多个元素,并返回数组操作后数组长度

let arr3 = [1, 2, 3];
let length2 = arr3.unshift(-1, 0)
console.log(arr3); // 输出: (5) [-1, 0, 1, 2, 3]
console.log(length2); // 输出: 5

shift(): 删除数组的第一个元素,并返回删除的元素

let arr4 = [1, 2, 3];
let result4 = arr4.shift();
console.log(arr4); // 输出: (2) [2, 3]
console.log(result4); // 输出: 1

splice(): 可以添加、插入、删除、修改元素,并把删除的元素作为数组返回

splice(start, deleteCount, item1, item2, /* …, */ itemN):
start: 开始脚标操作的脚标
deleteCount: 从开始操作脚标开始往后删除的元素个数
item1, item2, ..., itemN: 添加的元素

添加

追加元素: splice(arr.length, 0, element1, element2, ..., elementN)

// 添加
let arr7 = [1, 2, 3];
let result7 = arr7.splice(arr7.length, 0, 4);
console.log(arr7); // 输出: (4) [1, 2, 3, 4]
console.log(result7); // 输出: []

删除

splice(索引,删除个数,添加元素1,添加元素2,...)

从index=1开始删除2个元素,删除了index=1,index=2的元素,返回被删除的元素作为数组

let arr5 = [1, 2, 3];
let result5 = arr5.splice(1, 2);
console.log(arr5); // 输出: (1) [1]
console.log(result5); // 输出: (2) [2, 3]

从index=1开始删除2个元素,删除了index=1,index=2的元素,追加5作为新元素,返回被删除的元素作为数组

let arr6 = [1, 2, 3];
let result6 = arr6.splice(1, 2, 5);
console.log(arr6); // 输出: (2) [1, 5]
console.log(result6); // 输出: (2) [2, 3]

修改

替换: splice(替换脚标, 1, 替换元素值)

// 替换
let arr8 = [1, 2, 3];
let result8= arr8.splice(1, 1, 5);
console.log(arr8); // 输出: (3) [1, 5, 3]
console.log(result8); // 输出: [2]

插入

插入: splice(插入脚标, 0, 插入元素值)

// 插入
let arr9 = [1, 2, 3];
let result9 = arr9.splice(2, 0, 4);
console.log(arr9); // 输出: (4) [1, 2, 4, 3]
console.log(result9); // 输出: []

reverse(): 把原始数组反转,并返回已经反转后的原始数组

// 反转
let arr10 = [1, 2, 3];
let result10 = arr10.reverse();
console.log(arr10); // 输出: (3) [3, 2, 1]
console.log(result10); // 输出: (3) [3, 2, 1]

数组去重

// 方式一
let arr12 = [1, 2, 3, 3, 2, 1, 10];
for (let i = 0; i < arr12.length; i++) {
    // 从该元素的下一个位置开始查找元素第一次出现索引值
    let index = arr12.indexOf(arr12[i], i + 1);
    // 如果索引值不为-1,说明该元素不是第一次出现,删除该元素
    if (index !== -1) {
        arr12.splice(index, 1);
        // 由于删除元素,脚标向前以移动,因此需要再次比较该位置的元素
        i--;
    }
}
console.log(arr12);

// 方式二
let arr13 = [1, 2, 3, 3, 2, 1, 10];
let newArr13 = [];
for (let i = 0; i < arr13.length; i++) {
    // 元素第一次出现的脚标和当前脚标相同,说明该元素不是重复的
    if (arr13.indexOf(arr13[i]) === i) {
        newArr13.push(arr13[i]);
    }
}
console.log(newArr13);

// 方式三
let arr11 = [1, 2, 3, 3, 2, 1, 10];
let result11 = arr11.filter((item, index, arr) => {
    return arr.indexOf(item) === index;
});
console.log(result11);

拷贝

拷贝: 以原对象为模板,创建一个新对象

浅拷贝slice()和深拷贝structuredClone()

如果对象中是原始值,那么拷贝对象不区分浅拷贝和深拷贝,因为对于原始值的拷贝不存在复制地址引用,而是重新复制一份新的原始值填充到新对象中

修改原始数组的原始值,不影响新数组的值

let arr = [1, 2, 3];
let newArr = arr.slice();
arr[0] = 2;
console.log(newArr); // 输出: [1, 2, 3]

浅拷贝: 对对象中的属性和值进行复制,并不会改变属性对应的引用地址

let obj = {};
let arrTest = [];
let arr2 = [obj, 2, arrTest];
let newArr2 = arr2.slice();
obj.test = 'test';
arrTest[0] = 'test';
console.log(newArr2);

arr2中obj和arrTest都是引用地址,slice方法只是复制了引用地址到新数组中,因此改变obj的内容,新数组中的obj元素的值也会改变
img

深拷贝: 对对象中的属性进行复制,而且还复制属性值为引用地址对应的内存区域,重新对属性进行赋值新的引用地址
structuredClone(): window宿主对象一个方法,用于深拷贝一个对象

let obj2 = {};
let arr3 = [obj2];
let newArr3 = structuredClone(arr3);
obj2.test = 'test';
console.log(newArr3);

修改obj的属性,通过深拷贝获得的newArr3中obj元素的值没有发生变化,说明newArr3中的obj和arr3中的obj不是同一个对象
img


展开运算符...进行对象浅拷贝

拷贝数组

...arr: 将arr中的元素依次取出,arr[0],arr[1],...arr[n]

let arr = [1, 2, 3];
let arr2 = [0, ...arr, 4];
console.log(arr2); // 输出: (5) [0, 1, 2, 3, 4]

使用场景

1.浅拷贝对象
2.函数传参

let arr = [1, 2, 3];
const sum = (a, b, c) => a + b + c;
console.log(sum(...arr)); // 输出: 6

拷贝普通对象

let obj = { name: '张三' };
console.log({ name: '王五', ...obj }); // 输出: {name: '张三'}
console.log({ name: '王五', ...obj, name: '赵六' }); // 输出: {name: '赵六'}

注意

新对象中和原始对象中有重复属性时,在新对象中后面的属性值会覆盖掉前面的属性值


对象浅拷贝Object.assign(目标对象,原始对象)

使用Object对象赋值方法: assign
Object.assign(目标对象,原始对象): 将原始对象的属性和值赋值给目标对象,并返回目标对象

let arr3 = [1, 2];
let obj = { name: '张三' };
let tempObj = { name0: '李四' };
let newObj = Object.assign({}, arr3);
let newObj2 = Object.assign({}, obj);
let newObj3 = Object.assign([], arr3);
let newObj4 = Object.assign(tempObj, obj);
let newObj5 = Object.assign(arr3, obj);
console.log(newObj); // 输出: {0: 1, 1: 2}
console.log(newObj2); // 输出: {name: '张三'}
console.log(newObj3); // 输出: (2) [1, 2]
console.log(newObj4); // 输出: {name0: '李四', name: '张三'}
console.log(newObj5); // 输出: (2) [1, 2, name: '张三']
console.log(newObj5.name); // 输出: 张三
console.log(Array.isArray(newObj5)); // 输出: true
console.log(arr3 === newObj5); // 输出: true
console.log({...obj}); // 输出: {name: '张三'}
console.log({ name: '王五', ...obj, name: '赵六' }); // 输出: {name: '赵六'}

img

注意

1.Object.assign(): 不改变原数组
2.Object.assign(): 不会修改目标对象已有的属性,只能在目标对象后面追加赋值的属性和值,如果目标对象已存在属性,则覆盖其值
3.数组赋值到对象,key: index value: 数组元素
4.通过assign方法将对象赋值到数组,在原数组后以key:value形式追加对象属性和值,但是新数组长度和原数组长度相同,新数组可以向对象一样取出对象赋值过来的属性值

let obj = {
  name: "hello",
  age: 1,
};
let arr = [];
Object.assign(arr, obj);
console.log(arr);

img


总结

数组浅拷贝: 数组.slice()[...数组]Object.assign(新数组, 原数组)
数组深拷贝: structuredClone(数组)

对象浅拷贝: {...原对象}Object.assign(目标对象, 原对象)
对象深拷贝: structuredClone(对象)


排序

冒泡排序

img

// 冒泡排序
let arr = [10, 2, 9, 40, 5];
for (let i = 0; i < arr.length; i++) {
    for (let j = 0; j < arr.length - i - 1; j++) {
        if (arr[j] > arr[j + 1]) { // 相邻元素比较
            const temp = arr[j];
            arr[j] = arr[j + 1];
            arr[j + 1] = temp;
        }
    }
}
console.log(arr);

选择排序

img

// 选择排序
let arr2 = [10, 2, 9, 40, 5];
for (let i = 0; i < arr2.length; i++) {
    let minIndex = i;
    for (let j = i + 1; j < arr2.length; j++) {
        if (arr2[minIndex] > arr2[j]) {
            minIndex = j; // 找到最小值的索引
        }
    }
    // 替换第i个元素为最小值
    let temp = arr2[i];
    arr2[i] = arr2[minIndex];
    arr2[minIndex] = temp;
}
console.log(arr2);

封装函数

// 封装年龄过滤条件
let ageCondition = function (arr) {
    let result = [];
    for (let i = 0; i < arr.length; i++) {
        if (arr[i].age >= 30) {
            result.push(arr[i]);
        }
    }
    return result;
}
// 封装姓名过滤条件
let nameCondition = function (arr) {
    let result = [];
    let names = ['张三', '王五'];
    for (let i = 0; i < arr.length; i++) {
        if (names.includes(arr[i].name)) {
            result.push(arr[i]);
        }
    }
    return result;
}
let obj1 = { name: '张三', age: 20 };
let obj2 = { name: '李四', age: 30 };
let obj3 = { name: '王五', age: 40 };
let arr = [obj1, obj2, obj3];
function filter(arr, conditionFn) {
    return conditionFn(arr);
}
console.log(filter(arr, ageCondition));
console.log(filter(arr, nameCondition));

img


高阶函数

高阶函数: 函数作为参数传递给另一个函数,或者函数作为返回值返回的函数,就称为高阶函数

函数作为参数传递给另一个函数

将一个匿名函数传递给filter函数

let obj1 = { name: '张三', age: 20 };
let obj2 = { name: '李四', age: 30 };
let obj3 = { name: '王五', age: 40 };
let arr = [obj1, obj2, obj3];

// filter为高阶函数,因为它可以接受一个函数作为参数
function filter(arr, conditionFn) {
    let result = [];
    for (let i = 0; i < arr.length; i++) {
        if (conditionFn(arr[i])) {
            result.push(arr[i]);
        }
    }
    return result;
}
console.log(filter(arr, arg => arg.name == '张三')); // [{name: '张三', age: 20}]

函数作为返回值

定义一个高阶函数返回一个函数,返回函数中包含了已有的函数逻辑

function myFn() {
    console.log('执行了myFn');
    return 'myFn';
}

// 定义一个高阶函数,返回一个函数
function extendFn(fn) {
    return function () {
        console.log('扩展fn功能的函数执行了');
        return fn();
    }
}
console.log(extendFn(myFn)()); // 扩展fn功能的函数执行了 执行了myFn myFn

高阶函数保护资源安全性,只能通过函数随时随地使用资源

// 指定函数执行时,才能访问资源,函数外部不可访问资源
function myFn() {
    // 保护起来的资源
    let resource = '资源';
    // 返回匿名函数
    return () => {
        return resource;
    };
}
console.log(myFn()()); // 输出: 资源
console.log(resource); // 错误: resource is not defined

在执行myFn返回的匿名函数过程中,该匿名函数使用了resource,因为resource和匿名函数在同一作用域,因此匿名函数可以访问到resource,而其他地方访问不到。
这就是高阶函数可以保护资源的作用。

总结高阶函数的作用

1.动态将运行逻辑传递给另一个函数去执行
2.动态生成新函数,新函数可扩展已有函数功能
3.保护资源不被外部直接使用,而是通过返回函数随时随地使用(闭包的作用之一封装私有变量)


闭包

闭包: 由封闭函数和封闭函数的作用域中的变量组成的函数。闭包让封闭函数能访问它的外部作用域。
闭包的形成: 函数嵌套函数,由函数创建时自动生成

如下代码,可以把myFn()看作闭包函数,匿名函数为内部函数,也正是内部函数(匿名函数)的存在才形成了闭包

function myFn() {
    // 保护起来的资源
    let resource = '资源';
    // 返回匿名函数
    return () => {
        return resource;
    };
}
console.log(myFn()()); // 输出: 资源
console.log(resource); // 错误: resource is not defined

构建闭包的必要条件

1.函数嵌套
2.内部函数引用外部函数的变量
3.外部函数返回内部函数

闭包的原理

词法作用: 词法作用域(Lexical Scope) 是定义表达式并能被访问的区间。 换言之,一个声明(定义变量、函数等)的词法作用域就是它被定义时所在的作用域。 注意: 词法作用域又叫静态作用域。

闭包机制中外层函数作用域销毁,为什么嵌套函数还能访问外层函数的变量

闭包通过保留对外部作用域的引用,“延长”了变量的生命周期,使得嵌套函数在父函数执行完毕后仍能访问其变量。这是JavaScript中闭包的核心特性之一

myFn()函数执行完毕,引用赋值给了resultFn,JS引擎为了确保resultFn后续能够被正常地调用,会让resultFn带上已引用的外层函数变量,而外层变量存储在自身的scopes区域中,因此resultFn仍然引用了闭包中的变量。
myFn()()myFn()执行后没有被任何变量引用,垃圾回收机制认为该对象无用,会被垃圾回收机制回收,闭包中的资源也会被回收,因此sum的值不会保留

function myFn() {
    let sum = 0;
    return () => {
        return sum++;
    };
}
let resultFn = myFn();
console.log(resultFn()); // 输出: 0
console.log(resultFn()); // 输出: 1
console.log(myFn()()); // 输出: 0
console.log(myFn()()); // 输出: 0

resultFn变量引用了闭包中的资源,把该资源放到自己的scopes区域中,及自身作用域中
img

闭包的生命周期

闭包的创建: 每次调用外层函数都会产生全新的闭包
闭包销毁: 当返回的嵌套函数未被引用时,闭包销毁

闭包的作用

闭包通过绑定函数与其词法作用域,实现了以下目标:

1.控制变量生命周期:延长局部变量的生存周期
2.实现封装与隔离:隐藏实现细节,减少全局污染
3.灵活的函数行为:支持高阶函数、工厂模式、柯里化等
4.状态管理:在异步、事件驱动编程中保留上下文

闭包和类的区别

1.类通过原型机制继承公共属性,占用内存小
2.闭包每次使用都会创建独立的闭包内存,占用内存大


递归

递归: 函数内部调用自身函数

不终止递归函数执行,会出现栈内存溢出

function fn() {
    fn();
}
fn();

img

递归逻辑书写条件和核心思想

1.终止条件
2.递归条件

// 函数功能返回一个数的阶层 4 * 3 * 2 * 1 = 24,因此4的阶乘是24
function factorial(num) {
    // 终止条件
    if (num === 1) {
        return 1; // 返回num等于1时该功能函数的返回值
    }
    /* 递归条件: 
    1.一定要返回这个数的递归结果  
    2.一定使用当前函数作为递归条件  
    分析
    一个数字乘以(一个数字-1)的阶乘 -> num * (num-1)! -> num * factorial(num-1)
    */
    return num * factorial(num - 1);
}

img
执行过程:
函数执行触碰到终止条件时开始由内向外执行return中的语句,直到执行完最外层return语句,而后返回结果
img

1.return factorial(4);          // 9.接收factorial(4)的返回值24,该return没有执行语句,该return语句执行结束,将结果24返回
    2.return factorial(3) * 4;      // 8.接收factorial(3)的返回值6,继续执行 6 * 4 = 24,该return语句执行结束,将结果24返回给上一层的递归函数
        3.return factorial(2) * 3;      // 7.接收factorial(2)的返回值2,继续执行 2 * 3 = 6,该return语句执行结束,将结果6返回给上一层的递归函数
            4.return factorial(1) * 2;      // 6.接收factorial(1)的返回值1,继续执行 1 * 2 = 2,该return语句执行结束,将结果2返回给上一层的递归函数
                5.return 1;                     // 该return语句执行结束,将结果1返回给上一层的递归函数

核心思想: 将大问题拆分为多个小问题,大问题和小问题使用相同逻辑得到结果


练习_兔子繁衍问题(斐波那契数列)

需求: 一对兔子出生两个月后每个月都能生一对兔子,计算第n个月总共多少对兔子

// 一对兔子出生两个月后每个月都能生一对兔子,计算第n个月总共多少对兔子
function sum(n) {
    if (n <= 2) {
        return 1;
    }
    /*
        月数: 对数
        1: 1 = 1
        2: 上月兔子 = 1
        3: 上月兔子 + 新兔子(1月兔子出生) = 1 + 1 = 2
        4: 上月兔子 + 新兔子(1月兔子出生) = 2 + 1 = 3 
        5: 上月兔子 + 新兔子(1月兔子出生+3月新兔子出生) = 3 + 1 + 1 = 5 
        6: 上月兔子 + 新兔子(1月兔子出生+3月新兔子出生+4月新兔子出生) = 5 + 1 + 1 + 1 = 8
        7: 上月兔子 + 新兔子(1月兔子出生+3月新兔子出生+4月新兔子出生+5月新兔子出生) = 8 + 1 + 1 + 1 + 2 = 13
        8: 上月兔子 + 新兔子(1月兔子出生+3月新兔子出生+4月新兔子出生+5月新兔子出生+6月新兔子出生) = 13 + 1 + 1 + 1 + 2 + 3 = 21
    */
    return sum(n - 1) + sum(n - 2);
}
console.log(sum(6));

递归的缺点

递归的缺点:
1.递归调用会消耗栈内存,当递归调用过深时,可能会导致栈内存溢出
2.栈内存嵌套太深时,执行效率会下降


数组方法使用回调函数

sort(): 排序后返回原数组,破坏性

sort: 默认升序排序,并以unicode编码进行排序,例如10 < 2,导致对数字排序混乱

let arr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
console.log(arr.sort()); // 输出: (10) [1, 10, 2, 3, 4, 5, 6, 7, 8, 9]

通过传递回调函数指定排序规则,避免默认sort方法处理数字类型排序混乱:

let arr2 = [10, 2, 1, 3, 40, 20];
// arg1 > arg2 升序
console.log(arr2.sort((a, b) => a - b)); // 输出: (6) [1, 2, 3, 10, 20, 40]

注意

sort回调函数返回值必须为数字类型整数,当a-b>0时,升序,当a-b<0时,降序


forEach(): 遍历数组元素

forEach(fn): 遍历数组的方法,接收一个回调函数,按照顺序读取元素,每读取一个元素,就会执行一遍回调函数

回调函数参数
fn(elment){}: 回调函数只有一个值时,表示当前元素
fn(elment, index){}: 回调函数有两个值时,表示当前元素和索引
fn(elment, index, arr){}: 回调函数有三个值时,表示当前元素、索引、数组本身

let arr3 = ['张三', '王五', '赵六'];
arr3.forEach(element => console.log(element)); // 输出: 张三 王五 赵六
arr3.forEach((element, index) => console.log(element, index)); // 输出: 张三 0 王五 1 赵六 2
        arr3.forEach((element, index, arr) => console.log(element, index, arr)); // 张三 0 (3) ['张三', '王五', '赵六'] 王五 1 (3) ['张三', '王五', '赵六'] 赵六 2 (3) ['张三', '王五', '赵六']

filter(): 遍历并过滤元素,返回新数组,非破坏性

filter(): 遍历并根据回调函数返回值(true/false)判断是否将元素放到新数组中,回调函数返回true,元素push到新数组,回调函数返回false,元素不会push到新数组

let arr4 = [1, 2, 'a', 'b', 'a'];
let newArr4 = arr4.filter(item => console.log(item)); // 输出: 1 2 a b a
console.log(newArr4); // 输出: []
let newArr5 = arr4.filter((item, index) => 1);
console.log(newArr5); // 输出: [1, 2, 'a', 'b', 'a']

回调函数参数
fn(item): 原数组元素
fn(item, index): 原数组元素和索引
fn(item, index, arr): 原数组元素、索引和原数组

对数组元素去重

let arr7 = [1, 2, 1, 'a', 'b'];
let newArr7 = arr7.filter((item, index, arr) => arr.indexOf(item) == index);
console.log(newArr7); // 输出: [1, 2, 'a', 'b']
console.log(arr7); // 输出: [1, 2, 1, 'a', 'b']

复制新数组

let arr8 = [1, 2, 1, 'a', 'b'];
console.log(arr8.filter(item => true));  // 输出: [1, 2, 1, 'a', 'b']

map(): 对数组元素进行加工,生成新数组,非破坏性

map(): 根据函数返回值,填充到新数组中

callbackFn(element, index, arr)

let arr9 = [1, 2, 1, 'a', 'b'];
let newArr9 = arr9.map(item => item);
console.log(newArr9); // 输出: [1, 2, 1, 'a', 'b']

reduce(): 合并数据元素,非破坏性

参数
1.回调函数
回调函数参数有两个: (pre, cur)
2.初始值(可选)

传入一个参数

reduce(fun(pre,cur)):
pre: 该值为上一次调用回调函数的结果,或者初始值
cur: 该值为当前被处理的元素

调用流程:
1.将组数中前两个元素作为参数运算,返回一个值
2.将返回值和第三个元素作为参数运算,返回一个值
3.将返回值和第四个元素作为参数运算,返回一个值
4.依次类推,直到数组中的所有元素都被运算过,返回一个值

let arr10 = [1, 2, 1, 'a', 'b'];
let newArr10 = arr10.reduce((pre, cur) => {
    return pre + cur;
});
// 1.return pre + cur; =>  1 + 2 = 3
// 2.return pre + cur; => 3 + 1 = 4
// 3.return pre + cur; => 4 + 'a' = '4a'
// 4.return pre + cur; => '4a' + 'b' = '4ab'
console.log(newArr10); // 输出: '4ab'

传入两个参数

reduce(fn(pre, cur), 初始值): reduce函数可以接收一个回调函数和一个初始值,初始值将作为回调函数中的第一个参数的值

let arr10 = [1, 2, 1, 'a', 'b'];
// 1.return pre + cur; =>  pre: 初始化 cur:第一个值 1 + 1  = 2
// 2.return pre + cur; => 2 + 2 = 4
// 3.return pre + cur; => 4 + 1 = 5
// 4.return pre + cur; => 5 + 'a' = '5a'
// 5.return pre + cur; => '5a' + 'b' = '5ab'
console.log(newArr10); // 输出: '5ab'

总结

1.数组中元素有几个,reduce中的函数就会执行几次
2.reduce中的函数会返回一个值,这个值会作为下次执行函数的第一个参数值,第二参数值始终为对应的当前脚标的元素值


数组解构赋值

let a, b, c;
let arr = [1, 2, 3];
[a, b, c] = arr; // 解构数组并赋值给变量
let [d, e, f, g] = arr; // 声明变量后结构数组并赋值给变量
console.log(a, b, c); // 输出: 1 2 3
console.log(d, e, f, g); // 输出: 1 2 3 undefined
[d, e, f, g] = [1, 2, 3, 4]; 
console.log(d, e, f, g); // 输出: 1 2 3 4

注意

1.每次解构赋值都会覆盖变量之前的值,即使变量之前有值,当前没有赋值,那么该变量的值就会变成undefined
2.可以指定变量默认值,默认值可以是一个变量

[a, b, c = 3] = [1,2];
console.log(a, b, c); // 输出: 1 2 3
[a, b, c = b] = [1,2];
console.log(a, b, c); // 输出: 1 2 2

3.可以使用...运算符来获取数组剩余部分,使用...数组名来接收剩余部分

[a,  b, ...c] = [1, 2, 3, 4, 5];
console.log(a, b, c); // 输出: 1 2 [3, 4, 5]

4.使用解构赋值快速交换两个变量值

let a1 = 1;
let a2=  2;
[a1, a2] = [a2, a1];
console.log(a1, a2); // 输出: 2 1

5.代码不加分号的问题

let [x] = [1]
[x] = [2] // 报错: ReferenceError: Cannot access 'x' before initialization
console.log(x);

原因: 由于JavaScript的自动分号插入(ASI)机制未按预期工作,导致两行代码被合并解析
let [x] = [1][x] = [1], 右侧表达式[1][x] = [2]试图访问xx已经声明了,但是并没有初始化,因此报错before initialization
类似:

console.log(g); // 报错: Cannot access 'g' before initialization
let g = 1;

5.二维数组结构

let arr2 = [[1, 2], [3, 4]];
let [[y, z]] = arr2;
console.log(y, z); // 输出: 1 2

对象解构赋值

let obj = { name: '张三', age: 18 };
let { name, age } = obj; // 声明解构对象并赋值
console.log(name, age); // 输出: 张三 18

如果先声明了变量,然后再解构赋值变量会报错,原因是{}被js解析成代码块,后面=则是语法错误
img
正确写法: 将结构代码使用括号包裹

let obj2 = { name: '张三', age: 18, sex: '男' };
let name, age;
({ name, age } = obj2);
console.log(name, age); // 输出: 张三 18

注意

1.变量名和对象中的属性名不一致时,会赋值为undefined

let obj3 = { name: '张三', age: 18, sex: '男' };
let { address } = obj3;
console.log(address); // 输出: undefined

2.对象中属性名和变量名不一致,需要指定变量别名,使用对象属性名:变量名的形式,而对象属性名也会声明但赋值为空字符串或报错is not defined

let obj4 = { name: '张三', age: 18, sex: '男' };
let { name: n, age: a } = obj4;
console.log(n, a); // 输出: 张三 18
console.log(name === ''); // 输出: true
console.log(age); // 输出: ReferenceError: age is not defined

3.对变量使用默认值,如果对象中没有该属性,则变量会赋值为默认值

let obj5 = { name: '张三', age: 18, sex: '男' };
let { name: n, age: a, address: ad = '北京' } = obj5;
console.log(n, a, ad); // 输出: 张三 18 北京

4.属性别名和默认值一起使用,使用对象属性名:变量名 = 默认值的形式
如果对象中有address就把值赋值给ad,如果没有就是用默认值,赋值给ad

let obj6 = { name: '张三', age: 18, sex: '男' };
let {address: ad = '北京' } = obj6;
console.log(ad); // 输出: 北京
let {name: ad = '北京' } = obj6;
console.log(ad); // 输出: 张三

5.解构赋值可以嵌套使用

let obj7 = { name: '张三', age: 18, sex: '男', address: { city: '北京', area: '朝阳区' } };
let { address: { city, area } } = obj7;
console.log(city, area); // 输出: 北京 朝阳区

JSON对象的序列化与反序列化

序列化: 将内存中的对象转化为可存储的格式,如JSON格式的字符串
反序列化: 将可存储的格式转化为内存中的对象,如JSON.parse(str)将字符串转为内存对象

JSON: JavaScript Object Notation(js对象表示法),是js内建对象(内置对象),属于工具类对象,可以直接使用,与大多数全局对象不同,JSON 不是一个构造函数。不能将它与 new 运算符 一起使用,也不能将 JSON 对象作为函数调用。JSON` 的所有属性和方法都是静态的(就像 Math 对象一样)

JSON.stringify(): 将对象序列化成字符串
JSON.parse(): 将字符串反序列化成对象

let obj8 = { name: '张三', age: 18, sex: '男' };
let str = JSON.stringify(obj8);
console.log(str); // 输出: {"name":"张三","age":18,"sex":"男"}
let obj9 = JSON.parse(str);
console.log(obj9); // 输出: { name: '张三', age: 18, sex: '男' }

JSON字符串中可以使用的数据类型

支持:
json字符串支持的数据类型

  1. 数字(整数和浮点数)
  2. 字符串(在双引号中)
  3. 逻辑值(true 和 false)
  4. 数组(在方括号中{})
  5. 对象(在花括号中[])
  6. null

不支持: undefined和symbol

let jsonStr = '{"name":"孙悟空","age":18,"good":true,"hobbies":["吃","喝","玩","乐"],"address":{"province":"北京市","city":"海淀区"},"job":null,"unknow":undefined}';
let obj = JSON.parse(jsonStr); // 报错: Unexpected token 'u', ...","unknow":undefined}" is not valid JSON
console.log(obj);

注意

1.JSON.stringify(null),返回字符串类型的null
console.log(typeof JSON.stringify(null)); // string类型的null
2.JSON.stringify(undefined),返回undefined类型的undefined
console.log(typeof JSON.stringify(undefined)); // undefined类型
3.JSON.parse(null),返回null类型
4.JSON.parse('null'),返回null类型
5.JSON.parse('undefined')报错: Uncaught SyntaxError: "undefined" is not valid JSON

使用JSON进行深拷贝

let obj10 = { name: '张三', age: 18, sex: '男', address: { city: '北京', area: '朝阳区' } };
let obj11 = JSON.parse(JSON.stringify(obj10));
console.log(obj10 === obj11); // 输出: false
posted @ 2025-06-10 15:17  ethanx3  阅读(19)  评论(0)    收藏  举报