[TOC]
函数(function)也是对象,可以理解为一个工具箱,里面存放着所需要用到的部分代码,在其他地方需要用到这部分代码时,只需调用这个函数即可。
//函数创建格式
function fn(){
console.log("我是一个函数");
}
//函数调用格式
fn();
函数的创建方法
声明函数的方式定义函数 function 函数名(参数){ }
函数表达式定义函数 const 函数名 = function(参数){ }
箭头函数 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);
作用域是指一个变量的可见区域。
全局作用域:
在script标签下直接编写的代码都位于全局作用域下。
全局作用域开始于页面打开,在页面关闭时销毁。
在全局作用域下声明的变量是全局变量,可以在任何位置访问。
块作用域:
在代码块{ }内编写的代码位于块作用域下。
块作用域开始于代码执行,在代码结束后销毁。
块作用域中声明的变量是局部变量,只可以在块作用域内部访问,在外部无法访问。
函数作用域:
函数作用域也是局部作用域。
函数作用域开始函数调用,在函数调用后销毁。
每次调用函数都会产生全新的作用域。
在函数内部声明的变量只能在函数内部访问。
作用域链:
当使用一个变量时,JS解释器会优先在当前代码块中寻找变量,如果未找到会一层一层向外寻找,如果最后在全局作用域中也未找到就会报错。
之前说到过对象的属性可以是任何值,包括对象、函数,当对象的属性指向函数的时候,此时便将这个属性称为方法。
let obj = {};
obj.name = "小明";
obj.age = 18;
obj.sayHello = () => {
console.log("hello");
}
console.log(obj);
obj.sayHello(); //称为调用obj的sayHello方法
window对象是浏览器提供的一个对象,可以直接访问。window对象也就是浏览器窗口,可以对浏览器窗口进行操作。
window对象负责存储JS的内置对象和浏览器的宿主对象。window对象的属性可以通过window对象访问,也可以直接访问。函数是window对象的方法。
var也可以声明变量,所声明的变量是位于全局作用域的,与let作用相同,但是无法作用于块作用域下。
var声明的变量都会作为window对象的属性保存。
function声明的函数作为window对象的方法保存。
let声明的变量不会作为window对象的属性保存,无法通过window对象访问。
var声明的变量虽然没有块作用域,但是有函数作用域。
当使用var声明变量时,声明会被提升,即变量会在所有代码执行前先被声明,直到执行到赋值语句时再进行赋值操作。
使用function声明函数时也会被提升,可以先调用,然后再使用function声明函数。
使用let声明变量也会被提升,但是赋值语句执行前,浏览器会禁止访问该变量。
立即执行函数是一个匿名函数,创建一个一次性的函数作用域,只调用一次,防止变量冲突。
(function(){
let a = 10;
console.log(a);
}());
(function(){
let a = 20;
console.log(a);
}())
this所指向的对象会根据函数调用的方式不同而不同。
以函数形式调用,this指向的对象是window
以方法形式调用,指向的对象是调用方法的对象
通过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创建对象时:
无法区分出不同类型的对象
无法批量创建对象
在JS中可以通过类(class)来解决这个问题:
类是对象模板,可以将对象中的属性和方法直接定义在类中,定义之后可以直接通过类来创建对象。
通过同一个类创建的对象称为同类型对象,可以通过 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
类是对象模板,要创建对象时第一件事应该是创建类。
类的代码块自动开启严格模式,只能设置对象的属性。
属性:
类代码块内直接设置的属性称为实例属性,只能通过实例来访问。
通过 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);
对象是存储不同属性的容器。不仅需要存储数据,而且还要保证数据的安全。而直接在对象中添加的属性不仅可以随意读取和修改,而且无法添加对属性修改的限制条件,所以此时对象中的数据是不安全的。
如何确保数据的安全:
私有化数据:将数据私有化,只能在类内部使用。
使用 getter 和 setter 方法来获取和修改属性。
使用 getter 和 setter 方法来开放数据的操作的好处:
可以限制对数据的读写。
可以在方法中添加对属性的验证。
封装主要是实现对数据安全的保护。
实现封装的方式:
属性私有化:在需要私有化的属性前加 #。(添加私有属性前需要在类中先进行声明操作)
使用 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);
对象存储属性的区域有两个:
对象自身
直接通过对象添加的属性存储于对象自身。
在类中通过 x = y 添加的属性位于对象自身。
原型对象 原型对象是除对象自身外在堆内存中新开辟的一个存储空间,对象中会有一些属性存储于原型对象中,存储于原型对象中的属性称为 _ _ proto _ _ ,原型对象也负责存储对象属性,当访问对象属性时,会优先访问对象自身的属性,如若没有才会去原型对象中寻找。
在类中通过 xxx(){ } 添加的方法存储与原型对象中。
主动向原型对象中添加的属性或方法。
访问一个对象的原型:
对象._ _ proto _ _
Object.getPrototypeOf(对象)
原型对象中的数据:
对象中的数据(属性、方法等)
对象的构造函数(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
修改原型: 原型就相当于人的基因一般,一般不建议修改原型。
原型修改通过 类.prototype 属性去修改。
一定不要通过实例对象去修改原型。
最好不要直接给 prototype 属性去赋值。
通过类的 prototype 属性可以访问实例的原型,通过类的 prototype 属性去修改原型的好处:
原型一经修改便修改了所有实例的原型对象。
不用创建新的实例。
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 :
instanceof 是检查一个对象是否是一个类的实例。
格式:对象 instanceof 类;
instanceof 检查的是对象的原型链上是否有该类实例,如若有则返回true。
Object 是所有对象的原型,所以任何对象与Object进行 instaceof 运算都会返回true。
hasOwn 是检查该属性是否属于对象自身(即没有存储于原型对象中)。是则返回true。
格式:Object.hasOwn(对象,"属性名");
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 调用函数时会发生如下事情:
创建一个新的Object对象。
将构造函数的 prototype 属性设置为新对象的原型。
使用实参来执行构造函数,构造函数的 this 指向新对象。
如果构造函数的返回值是原始值或没有返回值,那么新对象会作为返回值返回(一般不会为构造函数指定返回值)
一切皆对象,面向对象的本质就是编写代码的所有操作都是通过对象实现的。
面向对象编程的步骤可以简单的分为:找对象和搞对象两步。
学习对象:
明确对象代表什么,有什么作用。
如何获取到对象。
如何使用该对象(对象的属性和方法)。
对象的分类:
内建对象
由ES标准定义的对象。
如:String、Number、Object、Function...
宿主对象
由浏览器提供的对象。
BOM、DOM
自定义对象
由开发人员自己创建的对象。
本文章使用limfx的vscode插件快速发布