JavaScript学习(二)

[TOC]

函数

函数(function)也是对象,可以理解为一个工具箱,里面存放着所需要用到的部分代码,在其他地方需要用到这部分代码时,只需调用这个函数即可。

//函数创建格式
function fn(){
    console.log("我是一个函数");
}
//函数调用格式
fn();

函数的创建方法

  1. 声明函数的方式定义函数 function 函数名(参数){ }

  2. 函数表达式定义函数 const 函数名 = function(参数){ }

  3. 箭头函数 const 函数名 = ( ) => { }

function fn1() {
    console.log("函数声明方式定义函数");
}
const fn2 = function(){
    console.log("函数表达式定义函数");
}
const fn3 = () => {
    console.log("箭头函数");
}
const fn4 = () => console.log("箭头函数");
fn1();
fn2();
fn3();
fn4();

参数

参数有形参(形式参数)和实参(实际参数)之分。

函数定义时可以声明若干个形参,在函数中形参是未被赋值的。

实参是调用函数时使用的参数,实参的值会在调用函数的同时赋值给对应的形参。实参可以是各种类型的数据,包括对象,函数等。

function sum(a, b) {
    console.log(a + b);
}
sum(1, 2);

形参可以设置默认值,当没有对应的实参赋值时,函数会使用默认值。

const fn = function(a = 10 , b = 20){
    console.log( a + b );
}
fn(); //30

箭头函数只有一个参数时,小括号可以省略不写

const fn = a => {
    console.log(a);
}
fn(1); //1

如上文所说,参数可以是任何数据类型,也可以是对象甚至函数

function fn(a) {
    a();
}
fn(() => {
    console.log("我是箭头函数");
});

返回值

函数的返回值是以关键字 return 来指定。

返回值就是函数执行的结果,函数调用完毕后返回值将作为结果返回。当函数执行结果返回时,表明函数执行完毕,return 后的任何代码都不会执行,都会是死代码。

函数没有 return 或 return 后不跟任何值所返回的返回值都是 undefined。

返回值可以是任何值,也可以是对象、函数。

function fn(a, b) {
    return a + b;
}
let result = fn(1, 2);
console.log(result);

箭头函数的返回值可以直接写在箭头后,可以省去大括号,但是当返回值是使用字面量创建的对象时,需要在外部添加一个小括号。

const fn = () => ({name:"小明"};
let result = fn();
console.log(result);

作用域

作用域是指一个变量的可见区域。

全局作用域:

  1. 在script标签下直接编写的代码都位于全局作用域下。

  2. 全局作用域开始于页面打开,在页面关闭时销毁。

  3. 在全局作用域下声明的变量是全局变量,可以在任何位置访问。

块作用域:

  1. 在代码块{ }内编写的代码位于块作用域下。

  2. 块作用域开始于代码执行,在代码结束后销毁。

  3. 块作用域中声明的变量是局部变量,只可以在块作用域内部访问,在外部无法访问。

函数作用域:

  1. 函数作用域也是局部作用域。

  2. 函数作用域开始函数调用,在函数调用后销毁。

  3. 每次调用函数都会产生全新的作用域。

  4. 在函数内部声明的变量只能在函数内部访问。

作用域链:

当使用一个变量时,JS解释器会优先在当前代码块中寻找变量,如果未找到会一层一层向外寻找,如果最后在全局作用域中也未找到就会报错。

方法

之前说到过对象的属性可以是任何值,包括对象、函数,当对象的属性指向函数的时候,此时便将这个属性称为方法。

let obj = {};
obj.name = "小明";
obj.age = 18;
obj.sayHello = () => {
    console.log("hello");
}
console.log(obj);
obj.sayHello(); //称为调用obj的sayHello方法

window对象

window对象是浏览器提供的一个对象,可以直接访问。window对象也就是浏览器窗口,可以对浏览器窗口进行操作。

window对象负责存储JS的内置对象和浏览器的宿主对象。window对象的属性可以通过window对象访问,也可以直接访问。函数是window对象的方法。

var也可以声明变量,所声明的变量是位于全局作用域的,与let作用相同,但是无法作用于块作用域下。

  • var声明的变量都会作为window对象的属性保存。

  • function声明的函数作为window对象的方法保存。

  • let声明的变量不会作为window对象的属性保存,无法通过window对象访问。

  • var声明的变量虽然没有块作用域,但是有函数作用域。

提升(了解即可)

  1. 当使用var声明变量时,声明会被提升,即变量会在所有代码执行前先被声明,直到执行到赋值语句时再进行赋值操作。

  2. 使用function声明函数时也会被提升,可以先调用,然后再使用function声明函数。

  3. 使用let声明变量也会被提升,但是赋值语句执行前,浏览器会禁止访问该变量。

立即执行函数(IIFE)

立即执行函数是一个匿名函数,创建一个一次性的函数作用域,只调用一次,防止变量冲突。

(function(){
    let a = 10;
    console.log(a);
}());
(function(){
    let a = 20;
    console.log(a);
}())

this

this所指向的对象会根据函数调用的方式不同而不同。

  1. 以函数形式调用,this指向的对象是window

  2. 以方法形式调用,指向的对象是调用方法的对象

通过this可以在方法中引用调用方法的对象。

let obj = {
    name: "小明",
    sayName: function () {
        console.log(this.name);
    }
}
obj.sayName();

箭头函数的this与其他函数不同,箭头函数的this不看是用什么形式调用,只与外层函数域的this有关

function fn() {
    console.log("fn-->", this);
};
const fn1 = () => {
    console.log("fn1-->", this);
};
let obj = {
    name: "小明",
    fn,
    fn1,
    sayHello() {
        console.log(this.name);
    },
    sayThis() {
        const t = () => {
            console.log("t-->", this);
        };
        t();
    }
};
obj.fn(); //obj
obj.fn1(); //window
obj.sayHello(); //小明
obj.sayThis(); //obj

严格模式

JS语法有正常模式和严格模式。 正常模式下JS的代码会尽量不报错。 但是严格模式下有些语法会被禁止使用,也就是会报错,而且也会提升代码运行的性能。

开启严格模式只需要在script标签下编写"use strict"即可,这样严格模式作用的是全局,而如果只想在某函数中开启严格模式,只需在函数中编写即可。

"use strict"
// a = 10; //报错
let a = 10; 
console.log(a);

面向对象

面向对象编程

我们所编写的代码都是对现实事物的抽象,具体事物可以由数据和功能(也就是行为)来具体表现,抽象到代码中就是对象的属性和方法,在代码世界里一切皆对象。

面向对象有三大特点:

  • 封装 ———— 安全性

  • 继承 ———— 扩展性

  • 多态 ———— 灵活性

当使用Object创建对象时:

  1. 无法区分出不同类型的对象

  2. 无法批量创建对象

在JS中可以通过类(class)来解决这个问题:

  1. 类是对象模板,可以将对象中的属性和方法直接定义在类中,定义之后可以直接通过类来创建对象。

  2. 通过同一个类创建的对象称为同类型对象,可以通过 instanceof 来检查该对象是否属于此类,如果某对象是通过某类创建,则称该对象为这一类的实例。

语法:

  • 创建类: class 类名{ }; //类名采用大驼峰命名法

  • 通过类创建对象: new 类();

class Person {

};
class Dog {

};
const p1 = new Person();
const p2 = new Person(); //p2和p1是同类型对象
const d1 = new Dog();
console.log(p1 instanceof Person); //true
console.log(d1 instanceof Person); //false

属性

类是对象模板,要创建对象时第一件事应该是创建类。

类的代码块自动开启严格模式,只能设置对象的属性。

属性:

  1. 类代码块内直接设置的属性称为实例属性,只能通过实例来访问。

  2. 通过 static 声明的属性称为静态属性,只能通过类访问。

class Person {
    name = "小明"; //实例属性,只能通过实例访问 p1.name
    age = 10;
    static test = "静态属性"; //静态属性,只能通过类访问 Person.test
}
const p1 = new Person();
console.log(p1);
console.log(p1.name);
console.log(Person.test);

方法

通过类创建的方法有实例方法和静态方法两种。

实例方法是直接在类中创建方法。实例方法的 this 指向当前实例。

静态方法是在类中使用static声明的方法。静态方法的 this 指向类对象。

class Person {
    name = "小明"; //实例属性,只能通过实例访问 p1.name
    age = 10;
    static test = "静态属性"; //静态属性,只能通过类访问 Person.test
    sayHello() {
        console.log("大家好,我是" + this.name);
    }; //实例方法
    static sayGoodbye() {
        console.log("我是静态方法");
    }; //静态方法
}
const p1 = new Person();
console.log(p1);
console.log(p1.name);
console.log(Person.test);
p1.sayHello();
Person.sayGoodbye();

构造函数

类解决了无法区分类型和无法批量创建对象的问题,但是也有很明显的问题,只能批量创建具有相同属性的对象,如若分别修改属性,那只会让代码更加复杂,达不到简洁的效果。

所以引入构造函数 constructor ,可以在构造函数中对实例的属性进行修改,构造函数是当调用通过类创建的对象时就会执行。

class Person {
    constructor(name, age, gender) {
        this.name = name;
        this.age = age;
        this.gender = gender;
    };
}
const p1 = new Person("小明", 18, "男");
const p2 = new Person("小红", 16, "女");
const p3 = new Person("小方", 17, "男");
console.log(p1);
console.log(p2);
console.log(p3);

封装

对象是存储不同属性的容器。不仅需要存储数据,而且还要保证数据的安全。而直接在对象中添加的属性不仅可以随意读取和修改,而且无法添加对属性修改的限制条件,所以此时对象中的数据是不安全的。

如何确保数据的安全:

  1. 私有化数据:将数据私有化,只能在类内部使用。

  2. 使用 getter 和 setter 方法来获取和修改属性。

使用 getter 和 setter 方法来开放数据的操作的好处:

  • 可以限制对数据的读写。

  • 可以在方法中添加对属性的验证。

封装主要是实现对数据安全的保护。

实现封装的方式:

  1. 属性私有化:在需要私有化的属性前加 #。(添加私有属性前需要在类中先进行声明操作)

  2. 使用 getter 和 setter 方法操作属性:

get 属性名(){
    return #属性;
};
set 属性名(参数){
    this.#属性 = 参数;
};
class Person {
    #name;
    #age;
    #gender;
    constructor(name, age, gender) {
        this.#name = name;
        this.#age = age;
        this.#gender = gender;
    };
    get name() {
        return this.#name;
    };
    set name(name) {
        this.#name = name;
    };
    get age() {
        return this.#age;
    };
    set age(age) {
        if (isNaN(age) || age > 0) {
            this.#age = age;
        };
    };
};
const p1 = new Person("小明", 18, "男");
console.log(p1);
p1.name = "小方"; //看似是在调属性,实则是在调用方法
console.log(p1.name);
p1.age = -12;
console.log(p1.age);
p1.gender = "女";
console.log(p1.gender);
console.log(p1); //gender: '女', #name: '小方', #age: 18, #gender: '男'

多态

多态为JS提供了更多的灵活性。在JS中不会检查函数参数的类型,任何数据都可以作为参数传递。如果要调用某个函数,无需指定类型,只要对象满足条件即可。

class Person {
    constructor(name) {
        this.name = name;
    };
};
class Dog {
    constructor(name) {
        this.name = name;
    };
};
const person = new Person("小明");
const dog = new Dog("旺财");
function sayHello(obj) {
    console.log("你好," + obj.name);
};
sayHello(dog);

继承

使用关键字 extends 来实现继承,继承的类称为子类,被继承的类称为父类(超类),继承可以在不修改一个类的基础上将其复制、扩展。继承可以将父类内容原封不动全部复制过来,减少代码的编写,如若有与父类的部分有不同的需求,可以通过重写方法来进行相应的修改。

若想在子类中调用父类的方法,需要通过 super 关键字来调用,若要修改属性,则必须重写构造函数方法,重写构造函数时第一行代码必须是 super() ,调用父类的构造函数。

class Animal {
    constructor(name) {
        this.name = name;
    };
    sayHello() {
        console.log("动物打招呼~");
    };
};
class Dog extends Animal {
    sayHello() {
        console.log("汪汪汪");
    };
};
class Cat extends Animal {
    constructor(name, age) {
        super(name);
        this.age = age;
    };
    sayHello() {
        super.sayHello();
        console.log("喵喵喵");
    };
};
const dog = new Dog("旺财");
const cat = new Cat("汤姆", 12);
dog.sayHello();
cat.sayHello();
console.log(dog);
console.log(cat);

对象的结构及原型

对象存储属性的区域有两个:

  1. 对象自身

    • 直接通过对象添加的属性存储于对象自身。

    • 在类中通过 x = y 添加的属性位于对象自身。

  2. 原型对象 原型对象是除对象自身外在堆内存中新开辟的一个存储空间,对象中会有一些属性存储于原型对象中,存储于原型对象中的属性称为 _ _ proto _ _ ,原型对象也负责存储对象属性,当访问对象属性时,会优先访问对象自身的属性,如若没有才会去原型对象中寻找。

    • 在类中通过 xxx(){ } 添加的方法存储与原型对象中。

    • 主动向原型对象中添加的属性或方法。

访问一个对象的原型:

  • 对象._ _ proto _ _

  • Object.getPrototypeOf(对象)

原型对象中的数据:

  1. 对象中的数据(属性、方法等)

  2. 对象的构造函数(constructor),也就是创建对象的类。

原型对象也是对象,所以也有它的原型对象,层层嵌套就生成了一条原型链,但是这条原型链并非无穷无尽,他的终止点是Object对象的原型,Object对象的原型没有原型,是Null。

  • 通过类创建的P对象:p对象 --> 原型 --> 原型 --> null

  • 直接创建的obj对象:obj --> 原型 --> null

原型链:读取对象属性时,会优先在对象自身中寻找属性,如若没有则会去对象的原型对象中寻找,如若没有则会继续去原型对象的原型对象中寻找,直到找到Object对象的原型,如若还未找到,则会返回undefined。

作用域链:用来寻找变量,找不到会报错。 原型链:用来寻找属性,找不到会undefined。

class Person {
    name = "小明";
    age = 18;
    sayHello() {
        console.log("你好,我是" + this.name);
    }; //该方法存储于原型对象中
    sayAge = () => {
        console.log("我" + this.age + "岁了");
    }; //该方法存储与对象自身中
};
const p1 = {};
const p = new Person();
console.log(p);
console.log(p.__proto__); //访问原型对象方法一
console.log(Object.getPrototypeOf(p)); //访问原型对象方法二
console.log(p.__proto__.__proto__);
console.log(p.__proto__.__proto__.__proto__); //null
console.log(p1.__proto__.__proto__); //null

原型的作用: 所有同类型的对象有着相同的原型对象,也就意味着有着相同的原型链,所以原型对象可以看作是同类型对象的一个公共区域。

  • 原型对象可以被该类的所有实例访问

  • 可以在该类中创建一个公共属性或方法来让所有该类的实例访问

  • 对象中有些值是自身独有的属性,但是一些方法是所用对象共用的,没必要多次创建,便可以将其添加在原型对象中供同类型的所有实例使用。

在JS中继承就是通过原型来实现的,继承时子类的原型就是父类的实例。

class Person {
    constructor(name, age) {
        this.name = name;
    };
    sayHello() {
        console.log("你好,我是" + this.name);
    };
};
class Animal {

};
class Cat extends Animal {

};
const p1 = new Person("小明");
const p2 = new Person("小方");
const cat = new Cat();
console.log(p1.__proto__);
console.log(p1.__proto__ == p2.__proto__); //true
console.log(cat.__proto__.__proto__.__proto__.__proto__); //null
//cat --> Animal实例 -->  object --> object原型 --> null

修改原型: 原型就相当于人的基因一般,一般不建议修改原型。

  1. 原型修改通过 类.prototype 属性去修改。

  2. 一定不要通过实例对象去修改原型。

  3. 最好不要直接给 prototype 属性去赋值。

通过类的 prototype 属性可以访问实例的原型,通过类的 prototype 属性去修改原型的好处:

  1. 原型一经修改便修改了所有实例的原型对象。

  2. 不用创建新的实例。

class Person {
    name = "小明";
    age = 18;
    sayHello() {
        console.log("你好,我是" + this.name);
    };
};
const p1 = new Person();
Person.prototype.fn = () => {
    console.log(this);
};
console.log(p1);
p1.sayHello();
p1.fn();
console.log(Person.prototype);

instanceof 和 hasOwn :

  1. instanceof 是检查一个对象是否是一个类的实例。

    • 格式:对象 instanceof 类;

    • instanceof 检查的是对象的原型链上是否有该类实例,如若有则返回true。

    • Object 是所有对象的原型,所以任何对象与Object进行 instaceof 运算都会返回true。

  2. hasOwn 是检查该属性是否属于对象自身(即没有存储于原型对象中)。是则返回true。

    • 格式:Object.hasOwn(对象,"属性名");

  3. in 运算符是用来检查属性是否在对象中,无论自身还是原型,只要存在便返回true。

    • 格式:"属性名" in 对象

class Animal { };
class Dog extends Animal {
    name = "旺财";
    sayHello() {
        console.log("汪汪汪");
    };
};
const dog = new Dog();
console.log(dog instanceof Dog); //true
console.log(dog instanceof Animal); //true
console.log(dog instanceof Object); //true
console.log("name" in dog); //true
console.log("sayHello" in dog); //true
console.log(Object.hasOwn(dog, "name")); //true
console.log(Object.hasOwn(dog, "sayHello")); //false

旧类(认识即可)

var Person = (function () {
    function Person(name, age) {
        this.name = name; //this指向新建的对象
        this.age = age;
    };
    //向原型对象中添加方法(或属性)
    Person.prototype.sayHello = () => {
        console.log(this.name);
    };
    //添加静态属性
    Person.staticProperty = "xxx";
    //添加静态方法
    Person.staticMethod = () => {
        console.log(123);
    };
    return Person;
}());
/*      直接调用 函数名() ,是普通函数
        如果通过 new 调用 new 函数名() ,这个函数就是一个构造函数 */
const p = new Person("小明", 18);
console.log(p);
//继承
var Animal = (function () {
    function Animal() { };
    return Animal;
}());
var Cat = (function () {
    function Cat() { };
//将Animal实例赋值给Cat,那么Cat的原型就是Animal,Cat也就继承到了Animal。
    return Cat;
    Cat.prototype = new Animal(); 
}());
const cat = new Cat();
console.log(cat);

new运算符: 当使用 new 去调用一个函数时,该函数会作为构造函数使用。

当使用 new 调用函数时会发生如下事情:

  1. 创建一个新的Object对象。

  2. 将构造函数的 prototype 属性设置为新对象的原型。

  3. 使用实参来执行构造函数,构造函数的 this 指向新对象。

  4. 如果构造函数的返回值是原始值或没有返回值,那么新对象会作为返回值返回(一般不会为构造函数指定返回值

面向对象总结

一切皆对象,面向对象的本质就是编写代码的所有操作都是通过对象实现的。

面向对象编程的步骤可以简单的分为:找对象搞对象两步。

学习对象:

  1. 明确对象代表什么,有什么作用。

  2. 如何获取到对象。

  3. 如何使用该对象(对象的属性和方法)。

对象的分类:

  1. 内建对象

    • 由ES标准定义的对象。

    • 如:String、Number、Object、Function...

  2. 宿主对象

    • 由浏览器提供的对象。

    • BOM、DOM

  3. 自定义对象

    • 由开发人员自己创建的对象。


本文章使用limfx的vscode插件快速发布