前端学习9:JavaScript--对象与原型
前言:适合有基础的同学入门尝试 / 复习回忆。
对象基础:
1.创建用户对象
const user = { // 属性(键值对) name: \"小岛\", age: 20, isAdmin: false,}
2.方法(函数属性)
sayHello() { console.log(`你好,我是${this.name}`);}
3.访问属性
console.log(user.name); // \"小岛\"user.sayHello(); // 调用方法
属性和方法操作:
1.添加新属性
user.email = \"Island@example.com\";
2.删除属性
delete user.isAdmin;
※3.遍历属性(重要!)
for (let key in user) { console.log(`${key}: ${user[key]}`);}
※4. 方法中的this(重点理解!)
const car = { brand: \"Toyota\", start() { console.log(`${this.brand}启动了!`); }};car.start(); // this指向car对象
原型和继承
1. 原型链:
每个JS对象都有隐藏的[[Prototype]] (原型)
属性,形成链条
同时, prototype
属性也是一个对象(称为原型对象),JavaScript 会自动为这个原型对象添加 constructor
属性,默认指向该函数本身
const animal = { eats: true};const rabbit = { jumps: true, __proto__: animal // 设置原型(实际开发用Object.create,下面案例会有,先略过)};console.log(rabbit.eats); // true(来自原型)console.log(rabbit.jumps); // true(自身属性)
2. 构造函数(传统实现方式)
// 1. 创建构造函数(首字母大写)function Person(name, age) { this.name = name; this.age = age;}// 2. 在原型上添加方法Person.prototype.introduce = function() { console.log(`我是${this.name},今年${this.age}岁`);};// 3. 创建实例const p1 = new Person(\"李四\", 30);p1.introduce(); // 调用原型方法// 原型关系验证console.log(p1.__proto__ === Person.prototype); // trueconsole.log(Person.prototype.__proto__ === Object.prototype); // true
老式方式的继承:
了解即可~这种方式是 ES6 类语法出现之前实现继承的标准方式,理解它有助于深入掌握 JavaScript 面向对象的本质 —— 基于原型的继承系统。
// 父类构造函数 - 用于初始化实例属性function Animal(name) { this.name = name; // 每个动物都有\"name\"属性}// 重点:在原型上定义方法 - 所有实例共享Animal.prototype.eat = function() { console.log(`${this.name}在吃东西`);};// 子类构造函数function Bird(name) { Animal.call(this, name); // 关键:调用父类构造函数,继承属性}// 继承方法Bird.prototype = Object.create(Animal.prototype);Bird.prototype.constructor = Bird;Bird.prototype.fly = function() { console.log(`${this.name}在飞翔`); // 子类特有方法,不会影响父类 Animal};// 创建Bird实例const bird = new Bird(\"小鸟\");// 调用继承自Animal的属性和方法console.log(bird.name); // 输出:\"小鸟\"bird.eat(); // 输出:\"小鸟在吃东西\"// 调用Bird自身的方法bird.fly(); // 输出:\"小鸟在飞翔\"
代码解析:
Animal.call(this, name)
这行代码非常重要
- 它调用了父类
Animal
的构造函数- 通过
call(this)
确保父类构造函数中的this
指向当前Bird
实例- 这样
Bird
实例就能继承Animal
的name
属性Object.create(Animal.prototype)
创建一个新对象,其原型指向Animal.prototype
- 把这个新对象赋值给
Bird.prototype
,使得Bird
实例能访问Animal
原型上的方法(如eat
)- 为什么要修复
constructor
?
- 因为上一步操作后,
Bird.prototype.constructor
会指向Animal
- 手动设置为
Bird
才符合逻辑(构造函数的原型的constructor
应该指向自身)
原型链结构解析
bird实例 →Bird.prototype → Animal.prototype → Object.prototype → null
↑ ↑ ↑
fly: function eat: function toString等通用方法
↑
constructor: Bird
关键知识点总结(重点)
- 属性继承:通过
父类.call(this, 参数)
在子类构造函数中实现 - 方法继承:通过
子类.prototype = Object.create(父类.prototype)
实现 - 构造函数修复:必须手动设置
子类.prototype.constructor = 子类
- 原型链:实例通过
__proto__
指向原型对象,形成链式查找结构
※3. class语法(ES6现代写法)
class Animal { // 构造函数:初始化动物名称 constructor(name) { this.name = name; } // 原型方法 speak() { console.log(`${this.name} 发出声音`); }}// extends继承class Dog extends Animal { constructor(name, breed) { super(name); // 调用父类构造函数,初始化名称 this.breed = breed; // 新增 breed 属性存储狗的品种 } // 方法重写 speak() { console.log(`${this.name} 汪汪叫!`); } // 新增方法 run() { console.log(`${this.name} 在奔跑`); }}// 使用const myDog = new Dog(\"旺财\", \"金毛\");myDog.speak(); // \"旺财 汪汪叫!\"myDog.run(); // \"旺财 在奔跑\"
代码解析
Animal 基类
- 定义了一个动物类
Animal
,包含:
- 构造函数
constructor(name)
:初始化动物名称- 原型方法
speak()
:输出动物发出声音的通用描述Dog 子类(继承自 Animal)
- 使用
extends
关键字实现继承- 构造函数
constructor(name, breed)
:
- 通过
super(name)
调用父类构造函数,初始化名称- 新增
breed
属性存储狗的品种- 方法重写:重写了父类的
speak()
方法,改为具体的 \"汪汪叫\"- 新增方法:添加了
run()
方法,是 Dog 类特有的行为使用类创建实例
const myDog = new Dog(\"旺财\", \"金毛\")
创建了 Dog 类的实例- 调用方法:
myDog.speak()
和myDog.run()
分别调用了重写的方法和新增方法
原型链深度图解 :
原型链:当访问一个对象的属性或方法时,JavaScript 会沿着原型链向上查找,直到找到为止
myDog实例 → Dog.prototype → Animal.prototype → Object.prototype → null
↑ ↑ ↑ ↑
breed run() speak()基础版 toString等通用方法
↑ ↑ ↑
name constructor constructor
代码解析:
第一层:myDog 实例
- 包含实例自身的属性:
name
(来自父类)和breed
(自身特有)第二层:Dog.prototype
- 包含 Dog 类定义的方法:
run()
和重写的speak()
- 包含 Dog 类的构造函数
第三层:Animal.prototype
- 包含 Animal 类定义的方法:原始的
speak()
方法- 包含 Animal 类的构造函数
第四层:Object.prototype
- 包含 JavaScript 所有对象都具有的通用方法,如
toString()
、hasOwnProperty()
等终点:null
- 原型链的末端,没有任何属性和方法
几个小疑问:
一、为什么 Dog 子类必须用 super(name)
而不能直接 this.name = name
?
原因一:原型链初始化顺序
子类实例的创建过程是:先初始化父类的属性和方法,再添加子类自己的特性。
当你在子类构造函数中使用 this
关键字时,JavaScript 要求必须先通过 super()
完成父类的初始化,否则会报错。
比如下面的代码会直接报错:
class Dog extends Animal { constructor(name, breed) { this.name = name; // 错误!必须先调用 super() this.breed = breed; }}
原因二:代码复用
父类 Animal
的构造函数已经实现了 this.name = name
的逻辑,子类通过 super(name)
调用父类构造函数,就不需要重复写 this.name = name
了,这正是继承的意义 —— 复用父类代码。
二、如果创建 Animal
类的实例,调用 speak()
会输出什么?
会输出父类定义的 “发出声音”,而不是 “汪汪叫”。
原因是:方法调用遵循 “就近原则”—— 优先使用当前类或实例自身的方法,找不到再沿原型链向上找。
举例说明:
// 创建 Animal 实例const animal = new Animal(\"小动物\");animal.speak(); // 输出:\"小动物 发出声音\"
animal
是Animal
的实例,它的原型链是:animal → Animal.prototype → Object.prototype → null
。- 调用
speak()
时,JavaScript 在Animal.prototype
上找到了speak()
方法(父类定义的版本),所以直接执行该方法,输出 “发出声音”。
而 Dog
实例 myDog
调用 speak()
时:
- 它的原型链是:
myDog → Dog.prototype → Animal.prototype → ...
。 - JavaScript 先在
Dog.prototype
上找到了重写后的speak()
方法(“汪汪叫”),就不会再去找父类的版本了。(子类重写会覆盖父类方法)
案例实操(在控制台尝试)
(1)原型链追溯
class A {}class B extends A {}const b = new B();console.log(b instanceof B); // trueconsole.log(b instanceof A); // trueconsole.log(b instanceof Object); // true// 手动检查原型链console.log( b.__proto__ === B.prototype, B.prototype.__proto__ === A.prototype, A.prototype.__proto__ === Object.prototype);
(2)方法覆盖实验
class Parent { msg = \"父类属性\"; show() { console.log(this.msg); }}class Child extends Parent { msg = \"子类属性\";}const obj = new Child();obj.show(); // 输出什么?为什么?
(3)静态方法
class User { static staticMethod() { console.log(\"我是静态方法,通过类名调用\"); }}User.staticMethod(); // 正确const u = new User();u.staticMethod(); // 报错
(4)思考题目
1.老式传统方式:以下代码输出什么?为什么?(可以回顾上面的传统方式)
function Foo() {}const f1 = new Foo();console.log(f1.constructor === Foo);console.log(Foo.prototype.constructor === Foo);
首先理解几个关键概念:
Foo
是一个构造函数f1
是通过new Foo()
创建的实例对象- 每个函数都有一个
prototype
(原型)属性- 每个对象都有一个
constructor
(构造函数)属性,指向创建它的构造函数第一行输出
f1.constructor === Foo
→true
- 当通过
new Foo()
创建f1
时,f1
本身并没有constructor
属性- JavaScript 会沿着原型链查找,在
Foo.prototype
上找到constructor
属性- 这个属性指向
Foo
构造函数,因此f1.constructor
最终指向Foo
第二行输出
Foo.prototype.constructor === Foo
→true
- 函数的
prototype
属性是一个对象(称为原型对象)- JavaScript 会自动为这个原型对象添加
constructor
属性,默认指向该函数本身- 因此
Foo.prototype.constructor
直接指向Foo
构造函数
补充延伸知识:
- 原型对象的
constructor
属性是区分对象类型的重要标识- 如果手动修改了原型对象(如实现继承时),需要手动修复
constructor
指向,否则会出现不符合预期的结果(如前面继承示例中修复Bird.prototype.constructor
的操作)
2.如何实现一个myInstanceof
函数,判断对象是否是某个类的实例?
3.class语法中,super关键字有几种用法?分别是什么?
- 作为函数:
super(...)
用于子类构造函数中,调用父类构造函数,初始化继承的属性。 - 作为对象:
super.xxx
用于子类方法中,访问父类的原型方法(普通方法中)或静态方法(静态方法中)。