面向对象编程(OOP)
简单说,对象就是一组**“键值对”(key-value)的集合**,是一种无序的复合数据集合
//创建对象user1,user2,user3
var user1=new Object();
var user2=Object();
let mySymbol = Symbol()
let user3 = {
name:"孙悟空",
age:18,
["gender"]:"男",
[mySymbol]:"特殊的属性",
hello:{
a:1,
b:true
}
}
对象的每一个键名又称为“属性”(property),它的“键值”可以是任何数据类型,比如数组,对象,函数,字符串,布尔类型等。
如果一个属性的值为函数,通常把这个属性称为“方法”,它可以像函数那样调用
函数和方法是同一个意思
var user = {
getName: function (name) {
return name;
}
};
user.getName("it")
var obj=new Object(); //创建对象
obj.name = "孙悟空" //向对象中添加属性:对象.属性名 = 属性值
obj.age = 18//创建属性方法一
obj["gender"] = "男"//创建属性方法二
let str = "address"
obj[str] = "花果山" // 等价于 obj["address"] = "花果山",使用[]操作属性时可以用变量,变量就不需要加引号了
obj.name="Tom Sun" // 修改属性
delete obj.name // 删除属性,delete是运算符
obj.f = Object()//对象的属性值可以是对象
obj.f.name = "猪八戒"
obj.f.age = 28
console.log(obj)
console.log("name" in obj)//用in运算符来检查对象中是否含有某个属性,语法: 属性名 in obj,如果有返回true,没有返回false
运行结果
通常属性名就是一个字符串,没有变量命名的限制,所以可以用关键字命名,也可以数字开头
但是如果属性名有太多特殊字符,不能直接使用,需要使用[""]
来设置
虽然如此,但强烈建议属性名也按照标识符的规范命名,建议使用驼峰命名法
var obj=new Object();
obj.name='Code6E';
obj.if="以if关键字命名属性"//不建议
obj["$*&^#%()"]="特殊字符的属性名";//不建议
console.log(obj["$*&^#%()"]);//控制台输出"特殊字符的属性名"
运行结果
也可以用symbol()作为属性名,获取这种属性时,也必须使用symbol, 使用symbol添加的属性,通常是那些不希望被外界访问的属性
let mySymbol = Symbol()
let newSymbol = Symbol()
obj[mySymbol] = "通过symbol添加的属性"
console.log(obj[mySymbol])//mySymbol相当于一把钥匙,只能通过mySymbol去读取symbol()属性的值
console.log(obj[newSymbol])//取不到结果,因为只能用创建symbol()属性时的变量名取symbol属性值
如果属性的值还是一个对象,就形成了链式引用
var user = {
name:"itbaizhan",
age:13,
container:{
frontEnd:["Web前端","Android","iOS"],
backEnd:["Java","Python"]
}
}
user.container.frontEnd // ["Web前端","Android","iOS"]
枚举属性,指将对象中的所有的属性全部获取
let obj = {
name:'孙悟空',
age:18,
gender:"男",
address:"花果山",
[Symbol()]:"测试的属性" // 符号添加的属性不能被枚举
}
for(let propName in obj){
console.log(propName, obj[propName])//不能写成obj.propName,这样写会显示undefined,因为.不能识别变量
}
运行结果
2. 静态方法Object.keys()
const object1 = {
a: 'somestring',
b: 42,
c: false
};
console.log(Object.keys(object1));
// Expected output: Array ["a", "b", "c"]
Object.assign() 静态方法将一个或者多个源对象中所有可枚举的自有属性复制到目标对象,并返回修改后的目标对象。Object.assign()是ES6的一个方法,将后面的对象与第一个对象合并,相同属性后面的覆盖前面的,第一个对象不存在的属性会原封不动存到该对象。第一个对象有而后面的对象没有的属性保持不变
const target = { a: 1, b: 2 };
const source = { b: 4, c: 5 };
const returnedTarget = Object.assign(target, source);
console.log(target);
// Expected output: Object { a: 1, b: 4, c: 5 }
console.log(returnedTarget === target);
// Expected output: true
对象的解构赋值与数组类似,但有些语法需要注意
const obj = { name: "孙悟空", age: 18, gender: "男" }
// 声明变量同时解构对象
let { name, age, gender } = obj
// 或 声明与解构赋值分开写
// let name, age, gender
// 分开写时为避免被JS解析器误认为是给语句块赋值而报错,可以在花括号外加小括号来避免这种误解
// ({ name, age, gender } = obj)
// 没有的属性返回undefined
let { address } = obj
console.log(name, age, gender)
// 这里取了别名。address:d="花果山"表示先在obj中寻找address属性,如果找到了就赋值给d,没有找到就取默认值"花果山"给d
let {name:a, age:b, gender:c, address:d="花果山"} = obj
console.log(a, b, c, d)
对象的序列化
,
const obj = {
name: "孙悟空",
age: 18,
}
// 将obj转换为JSON字符串
const str = JSON.stringify(obj) //JSON.stringify() 可以将一个对象转换为JSON字符串
const obj2 = JSON.parse(str) // JSON.parse() 可以将一个JSON格式的字符串转换为JS对象
console.log(obj)
console.log(str) // {"name":"孙悟空","age":18}
console.log(obj2)
const str2 = '["hello", true, []]'
console.log(str2)
let a = 10
let b = 10
a = 12 // 当我们为一个变量重新赋值时,绝对不会影响其他变量, 只是改变了变量中存储的地址
console.log("a =", a)
console.log("b =", b)
let obj = Object()
obj.name = "孙悟空"
obj.age = 18
let obj2 = Object()
let obj3 = Object()
console.log(obj2 == obj3) // false
let obj4 = obj
console.log(obj === obj4)//true
obj4.name = "猪八戒" // 当修改一个对象时,所有指向该对象的变量都会收到影响
console.log("obj", obj)
console.log("obj4", obj4)
在使用变量存储对象时,很容易因为改变变量指向的对象,提高代码的复杂度, 所以多数情况下,声明存储对象的变量时会使用const(是否使用const声明对象看具体需求)
const只是禁止变量被重新赋值,不影响修改对象
const obj={
name:"孙悟空"};
var obj2=obj;
obj2.name="猪八戒";//修改对象
console.log(obj);
var obj3={};
obj=obj3;//报错
实时效果反馈
1. 下列关于对象描述正确的是:
A 对象是条件语句,负责进行判断
B 对象是一段反复调用的代码块
C 对象就是一组“键值对”(key-value)的集合
D 对象是按次序排列的一组值。每个值的位置都有编号(从0开始)
答案
1=>C
B是对函数的描述,D是对数组的描述
普通函数的this由调用函数的方式决定,与函数创建方式无关
function fn() {
// console.log(this === window)
console.log("fn打印", this)
}
const obj = { name: "孙悟空" }
obj.test = fn
const obj2 = { name: "猪八戒", test: fn }
fn()//这里以函数形式调用函数且fn()方法属于window对象,所以这里this指向window对象
window.fn()//这里以函数形式调用函数。这种写法与fn()等效
obj.test() // {name:"孙悟空",test:fn()}
obj2.test() // {name:"猪八戒", test:fn()}
const obj3 = {
name: "沙和尚",
sayHello: function () {
console.log(this.name)
},
}
const obj4 = {
name: "唐僧",
sayHello: function(){
console.log(this.name)
}
}
// 为两个对象添加一个方法,可以打印自己的名字
obj3.sayHello()
obj4.sayHello()
箭头函数的this由创建箭头函数时的外层作用域决定,和它的调用方式无关
function fn() {
console.log("fn -->", this)
}
const fn2 = () => {
console.log("fn2 -->", this) // 总是window
}
fn() // window
fn2() // window
const obj = {
name:"孙悟空",
fn, // fn:fn,对象的方法可以省略属性名而直接写函数
fn2,
sayHello(){
console.log(this.name)
// function t(){
// console.log("t -->", this)
// }
// t()
const t2 = () => {
console.log("t2 -->", this)
}
t2()
}
}
obj.fn() // obj
obj.fn2() // window
obj.sayHello()
作用:
异同
fn.bind() fn()
等效,所以用bind()指定this后别忘了调用函数或将返回值赋值给一个变量根据函数调用方式的不同,this的值也不同:
function fn() {
console.log("函数执行了~", this)
}
// 以函数形式调用,this是window
fn()
const obj = { name: "孙悟空", fn }
// 以方法形式调用,this是调用方法的对象
obj.fn()
调用函数除了通过函数名()
这种形式外,还可以通过其他的方式来调用函数
比如,我们可以通过调用函数的call()或apply()方法来调用函数
格式:函数.call()
或函数.apply()
fn.call(obj)
fn.apply(obj)
function fn2(a, b) {
console.log("a =", a, "b =", b, this)
}
fn2.call(obj, "hello", true)
fn2.apply(obj, ["hello", true])
平常不用指定函数的this对象时就以函数名()
形式直接调用函数,如果需要指定不是函数默认的this对象,就用函数名().call()
或函数名().apply()
来调用对象,用call()还是apply()就看参数类型,如果参数是需要一个个单独指定就用call(), 如果参数是数组就用apply()
function fn(a, b, c) {
console.log("fn执行了~~~~", this)
console.log(a, b, c)
}
const obj = {name:"孙悟空"}
// newFn()的this被指定为obj,参数固定为10,20,30, 后期无法修改。不影响原来的函数
const newFn1 = fn.bind(obj, 10, 20, 30)
newFn1()
const newFn2 = fn.bind(obj, 10)
newFn2()
箭头函数没有自身的this,它的this由外层作用域决定,
//这里箭头函数的this为外层作用域的this,即window()
const arrowFn = () => {
console.log(this)
}
// 无法修改
arrowFn.call(obj)
// 无法修改
const newArrowFn = arrowFn.bind(obj)
newArrowFn()
class MyClass {
fn = () => {
console.log(this)
}
}
//这里箭头函数的this为外层作用域的this,即实例mc的this
const mc = new MyClass()
// 无法修改
mc.fn.call(window)
const obj2 = {}
// Object()对象的属性名默认是省略了引号,特殊属性名需要用中括号`[]`括起
const obj = {
"name":"孙悟空",
'age':18,
[Symbol()]:"哈哈",
[obj2]:"嘻嘻"// 因为Object()对象的属性名(键)不能是对象,这里会将属性名转换成[object Object]
}
console.log(obj)
// 以下不管[]内是什么样的对象,一律会转换成字符串"object Object",所以都能读取到属性值"嘻
console.log(obj[obj2])
console.log(obj[{}])
创建Map对象:new Map()
属性和方法:
map.key
或map[key]
获取值,否则会返回undefined// 创建一个Map
const map = new Map()
map.set("name", "孙悟空")
map.set(obj2, "呵呵")
map.set(NaN, "哈哈哈")
map.delete(NaN)
// map.clear()
console.log(map)
console.log(map.get(obj2))
console.log(map.has("name"))
arr = Array.from(map)
或arr = [...map]
const map = new Map()
map.set("name", "孙悟空")
map.set("age", 18)
map.set({}, "呵呵")
// 将map转换为数组
// const arr = Array.from(map) // [["name","孙悟空"],["age",18],[{},"呵呵"]]
const arr = [...map]
console.log(arr)
//可以用二维数组去创建Map对象
const map2 = new Map([
["name", "猪八戒"],
["age", 18],
[{}, () => {}],
])
console.log(map2)
const map = new Map()
map.set("name", "孙悟空")
map.set("age", 18)
map.set({}, "呵呵")
// 将map对象的每一个键值对解构分别赋值给key,value
for (const [key, value] of map) {
// const [key, value] = entry
console.log(key, value)
}
// 可以用数组的forEach(element, index, array)方法遍历Map对象,对于Map对象forEach(value, key, map)它的参数分别为第一个参数表示值,第二个参数表示键,这里没有写出的第三个参数表示遍历的Map对象本身
map.forEach((value, key)=>{
console.log(value, key)
})
for(const key of map.keys()){
console.log(key)
创建Set对象
new Set()
new Set(数组)
// 创建一个Set
const set = new Set()
// 向set中添加数据
set.add(10)
set.add("孙悟空")
set.add(10)//重复的10加不进去
console.log(set)
// 遍历Set对象的元素
for(const item of set){
console.log(item)
}
// Set对象本质上是键值对相同的Map对象, Set对象可以使用一些(不是全部)Map对象的方法
for(const item of set.values()){
console.log(item)
}
// 将set转换成数组
const arr = [...set]
console.log(arr)
// 可以通过Set对象实现数组去重。思路:重复的数组->通过Set()函数转换成没有重复元素的Set对象->再把该
Set对象转换成数组
const arr2 = [1,2,3,2,1,3,4,5,4,6,7,7,8,9,10]
const set2 = new Set(arr2)
console.log([...set2])
window.alert(123)
与alert(123)
等效, window.console.log("哈哈")
与console.log("哈哈")
等效alert(window)
window.a = 10 // 向window对象中添加的属性会自动成为全局变量(即在最外层用var声明的变量)
console.log(a)//10
let b=33
console.log(window.b)//控制台输出undefined, 使用let声明的变量不会存储在window对象中,而存在一个秘密的小地方(无法通过window.c访问)
function fn2(){
//var d = 10 // var虽然没有块作用域,但有函数作用域
d = 10 //在局部作用域中,如果没有使用var或let声明变量,相当于window.d, 所以变量会自动成为window对象的属性, 也就是全局变量
}
fn2()
console.log(d)//10
Math是 JavaScript 的原生对象,提供各种数学功能。
let math= new Math()
这样写会报错。Math.abs
方法返回参数值的绝对值
Math.abs(1) // 1
Math.abs(-1) // 1
Math.max
方法返回参数之中最大的那个值,Math.min
返回最小的那个值。如果参数为空, Math.min
返回Infinity
, Math.max
返回-Infinity
。
Math.max(2, -1, 5) // 5
Math.min(2, -1, 5) // -1
Math.min() // Infinity
Math.max() // -Infinity
Math.floor
方法返回小于参数值的最大整数,floor原意是地板
Math.floor(3.2) // 3
Math.floor(-3.2) // -4
Math.ceil
方法返回大于参数值的最小整数, ceil原意是天花板
Math.ceil(3.2) // 4
Math.ceil(-3.2) // -3
Math.random()
返回0到1之间的一个伪随机数,0=<Math.random()<1
Math.random() // 0.28525367438365223
任意范围的随机数生成函数如下
function getRandomArbitrary(min, max) {
return Math.random() * (max - min) + min;
}
getRandomArbitrary(5, 10)
实时效果反馈
1. 下列代码获得一个非负整数,横线处应该填写的内容是:
function ToInteger(x) {
x = Number(x);
return ___(___(x));
}
ToInteger(-10.4); // 向下取整:10
A Math.floor
Math.abs
B Math.ceil
Math.abs
C Math.ceil
Math.min
D Math.floor
Math.min
答案
1=>A
详情参考MDN
Date
对象是 JavaScript 原生的时间库。它以1970年1月1日00:00:00作为时间的零点,可以表示的时间范围是前后各1亿天(单位为毫秒)
1s=1000ms
在JS中所有的和时间相关的数据都由Date对象来表示
因为时间的单位与进制比较多,为了便于计算,计算机底层使用的都是时间戳
时间戳是指格林威治时间1970年01月01日00时00分00秒(相当于北京时间1970年01月01日08时00分00秒)起至现在的总毫秒数。
格林威治和北京时间就是时区的不同
Unix是20世纪70年代初出现的一个操作系统,Unix认为1970年1月1日0点是时间纪元。JavaScript也就遵循了这一约束
Date.now()
:返回当前时间距离1970年1月1日 00:00:00 UTC 的毫秒数如果想创建一个指定时间的Date对象,可以以let d = new Date(时间字符串/时间参数)
的形式创建
字符串格式有:月/日/年 时:分:秒
、年-月-日T时:分:秒
Date()函数的参数分别为:Date(年, 月, 日, 时, 分, 秒, 毫秒)
示例代码如下
d = new Date("2019-12-23T23:34:35")
d = new Date("12/23/2019 23:34:35")
d = new Date(2019, 11, 23, 23, 34, 35)//注意参数形式时月份是从0开始,0对应1月,1对应2月,以此类推... 个人喜欢这种写法,不容易搞混
let date = new Date() // 直接通过new Date()创建时间对象时,实例对象存储的时间是被创建的时间,即该条语句执行的时间
var d = new Date('January 6, 2022');
d.getDate() // 6
d.getMonth() // 0
d.getYear() // 122
d.getFullYear() // 2022
编写函数获得本年度剩余天数
function leftDays() {
var today = new Date();
var endYear = new Date(today.getFullYear(), 11, 31, 23, 59, 59, 999);//年,月,日,时,分,秒,毫秒
var msPerDay = 24 * 60 * 60 * 1000;//一天的毫秒数
return Math.round((endYear.getTime() - today.getTime()) / msPerDay);//endYear-today得到的是秒数,不是标准的时间戳,所以要用getTime函数
}
实时效果反馈
1. 下列代码计算本年度剩余天数,划横线处应该填写代码是:
function leftDays() {
var today = new Date();
var endYear = new Date(today.___, 11, 31, 23, 59, 59, 999);
var msPerDay = 24 * 60 * 60 * 1000;
return Math.round((endYear.___ - today.___) / msPerDay);
}
A getTime() getDate() getTime()
B getTime() getTime() getDate()
C getFullYear() getTime() getDate()
D getFullYear() getTime() getTime()
答案
1=>D
上面通过getDate()、getDay()、getFullYear、getMonth()、getHours()、toLocaleString()等方法获取当地日期时间再转化成相应的时间格式比较麻烦。Date对象内置了获取指定格式日期时间的方法
这里只列出一部分,详见MDN
toLocaleDateString():方法返回指定日期对象日期部分的字符串,该字符串格式因不同语言而不同。
toLocaleTimeString():返回指定日期对象时间部分的字符串,该字符串格式因不同语言而不同。
toLocaleString():返回指定日期对象的日期时间的字符串,该字符串格式因不同语言而不同
功能:可以将一个日期转换为本地时间格式的字符串
const d = new Date()
let result1 = d.toLocaleDateString() // 将日期转换为本地格式的字符串
console.log(result1)
result1 = d.toLocaleTimeString() // 将时间转换为本地格式的字符串
console.log(result1)
result2 = d.toLocaleString("zh-CN", {
//对象参数中没写相应的属性则不会显示相应的时间,比如没写有关年份的属性则不会显示年份
year: "numeric",
month: "long",
day: "2-digit",
weekday: "short",
})
console.log(result2)
类与对象的关系:类是对象的模板/抽象,对象是类的实例/具体
使用Object创建对象的问题:
// const Person = class {}
// Person类专门用来创建人的对象
class Person{
}
// Dog类式专门用来创建狗的对象
class Dog{
}
const p1 = new Person() // 调用构造函数创建对象
const p2 = new Person()
const d1 = new Dog()
const d2 = new Dog()
console.log(p1 instanceof Person) // true
console.log(d1 instanceof Person) // false
类的代码块,默认就是严格模式。类的代码块是用来设置对象的属性的,不是什么代码都能写。比如在类中不能写let b;
类中的属性分为:
class Person{
name = "孙悟空" // Person的实例属性name p1.name
age = 18 // 实例属性只能通过实例访问 p1.age
static test = "test静态属性" // 使用static声明的属性,是静态属性(类属性) Person.test
static hh = "静态属性" // 静态属性只能通过类去访问 Person.hh
}
const p1 = new Person()
const p2 = new Person()
console.log(p1)
console.log(p2)
同样的,类的方法有实例方法和静态方法(类方法)
class Person{
name = "孙悟空"
// sayHello = function(){
// } // 添加方法的一种方式
sayHello(){
console.log('实例方法' + this)
} // 实例方法,通过实例来调用,实例方法中this就是当前实例
static test(){
console.log("我是静态方法", this)
} // 静态方法(类方法),用static声明,通过类来调用,静态方法中this指向的是当前类
}
const p1 = new Person()
// console.log(p1)
Person.test()
p1.sayHello()
JS通过包装类,使得我们可以直接调用原始值的属性与方法,格式为原始值.方法
或装载原始值的变量.方法
下面讲解其原理
JS中一共有5个包装类
String --> 字符串包装为String对象
Number --> 数值包装为Number对象
Boolean --> 布尔值包装为Boolean对象
BigInt --> 大整数包装为BigInt对象
Symbol --> 符号包装为Symbol对象
let num = 11
// 例如这里原始值num可以像对象一样直接调用Number对象的方法
num = num.toString() // "11"
console.log(num)// "11"
110.toString()// "110"
当我们在类中直接指定实例属性的值时,意味着我们创建的所有对象的属性都是这个值
class Person{
name="孙悟空"
age=18
gender="男"
sayHello(){
console.log(this.name)
}
}
const p1 = new Person()
const p2 = new Person()
const p3 = new Person()
//实例的属性都相同,这样不够灵活
console.log(p1)
console.log(p2)
console.log(p3)
这时可以在类中添加构造函数constructor(),构造函数的名字是固定的。构造函数会在我们调用类创建对象时执行
class Person{
constructor(name, age, gender){
// console.log("构造函数执行了~", name, age, gender)
// 可以在构造函数中,为实例属性进行赋值
// 在构造函数中,this表示当前所创建的对象
this.name = name
this.age = age
this.gender = gender
}
}
const p1 = new Person("孙悟空", 18, "男")
const p2 = new Person("猪八戒", 28, "男")
const p3 = new Person("沙和尚", 38, "男")
console.log(p1)
console.log(p2)
console.log(p3)
面向对象的特点:封装、继承和多态
封装主要用来保证数据的安全
“装”:对象就是一个用来存储不同属性的容器
“封”:对象不仅存储属性,还要负责数据的安全
直接添加到对象中的属性,并不安全,因为它们可以被任意的读取与修改
如何确保数据的安全:
1.私有化数据:将需要保护的数据设置为私有,只能在类内部使用。格式为在变量前加#
,私有变量需要在类内部先声明才能使用
2.提供setter和getter方法来开放和控制对数据的操作
属性设置私有,通过getter setter方法操作属性带来的好处
class Person {
// #address = "花果山" // 实例使用#开头就变成了私有属性,私有属性只能在类内部访问
#name
#age
#gender
constructor(name, age, gender) {
this.#name = name
this.#age = age
this.#gender = gender
}
//以下5个函数是读取与修改属性的老方法
sayHello() {
console.log(this.#name)
}
// getter方法,用来读取属性
getName(){// p1.getName(),以这种形式读取属性
return this.#name
}
// setter方法,用来设置属性
setName(name){// p1.setName('猪八戒'),以这种形式修改属性
this.#name = name
}
getAge(){
return this.#age
}
setAge(age){
if(age >= 0){//控制年龄为非负数
this.#age = age
}
}
//以下2个函数是读取与修改属性的新方法,这样可以直接以属性而不是函数的形式读取和修改属性
get gender(){//p1.gender
return this.#gender
}
set gender(gender){//p1.gender = "女",以这种形式修改属性
this.#gender = gender
}
}
const p1 = new Person("孙悟空", 18, "男")//创建新对象的参数会传递到构造函数
p1.setAge(-11) //年龄不被修改
console.log(p1.gender)
多态
class Person{
constructor(name){
this.name = name
}
}
class Dog{
constructor(name){
this.name = name
}
}
const dog = new Dog('旺财')
const person = new Person("孙悟空")
function sayHello(obj){
// if(obj instanceof Person){//不检查对象的类型,这种特点叫做多态
console.log("Hello,"+obj.name)
// }
}
sayHello(dog)
继承
面向对象编程的3大特点:
封装 —— 安全性
继承 —— 扩展性
多态 —— 灵活性
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()
super(name) //需要传递参数给父类时,在super中写上相应的参数,调用父类的构造函数
this.age = age
}
sayHello(){
// 调用一下父类的sayHello
super.sayHello() // 在方法中可以使用super来引用父类的方法
console.log("喵喵喵")
}
}
const dog = new Dog("旺财")
const cat = new Cat("汤姆", 3)
dog.sayHello()
cat.sayHello()
console.log(dog)
console.log(cat)
对象中存储属性的区域实际有两个:
class Person {
name = "孙悟空"//存到对象自身中
age = 18
// constructor(){
// this.gender = "男"
// }
sayHello() {//存到原型对象中
console.log("Hello,我是", this.name)
}
}
const p = new Person()
// p.address = "花果山"
// p.sayHello = "hello"
console.log(p.sayHello)
对象的隐式原型(实例对象.__proto__)===该对象构造函数的显式原型(Constructor.prototype)
访问一个对象的原型对象的方法
对象.__proto__
,注意这种方法不要以对象.__proto__ = 值
这种方式赋值,这样会产生意想不到的后果类.prototype
或构造函数.prototype
,这是以类的属性方式去访问原型Object.getPrototypeOf(对象/实例)
。这种方法避免了轻易被赋值的问题class Person {
name = "孙悟空"
age = 18
sayHello() {
console.log("Hello,我是", this.name)
}
}
const p = new Person()
const obj = {} //这是直接创建的对象,所以obj的父类是Object,obj.__proto__即Object.prototype,为原型链的尽头
类.prototype=实例/对象.proto,即类的显式原型等于其实例的隐式原型
原型对象也有原型,这样就构成了一条原型链,根据对象的复杂程度不同,原型链的长度也不同
p.__proto__ = Person.prototype --> Person.prototype .__proto__=Object.__proto__ --> Object.__proto__.__proto__ = null
obj.__proto__ = Object.prototype --> Object.prototype .__proto__ = null
class Person {
name = "孙悟空"
age = 18
sayHello() {
console.log("Hello,我是", this.name)
}
}
class Dog {}
const p = new Person()
const p2 = new Person()
console.log(p==p2)//false
console.log(p.__proto__ === p2.__proto__)//true
p.sayHello = "hello"
const d = new Dog()
const d2 = new Dog()
class Animal{
}
class Cat extends Animal{
}
class TomCat extends Cat{
}
const cat = new Cat()
// TomCat --> cat --> Animal --> Object --> Object原型 --> null
// cat --> Animal --> Object --> Object原型 --> null
// p.__proto__=Person.__proto__ --> object --> Object原型 --> null
console.log(cat.__proto__)//Animal
console.log(cat.__proto__.__proto__)//Object
console.log(cat.__proto__.__proto__.__proto__);//Object.__proto__即Object的原型
console.log(cat.__proto__.__proto__.__proto__.__proto__)//null
所有的同类对象它们的原型对象都是同一个,也就意味着,同类对象的原型链是一样的
原型就相当于是一个公共的区域,可以被所有该类实例访问,可以将该类实例中,所有的公共属性(方法)统一存储到原型中, 这样我们只需要创建一个属性,即可被所有实例访问
JS中继承就是通过原型来实现的,当继承时,子类的原型就是一个父类的实例
在对象中有些值是对象独有的,像属性(name,age,gender)每个对象都应该有自己值,但是有些值对于每个对象来说都是一样的,像部分方法和属性,对于一样的值没必要重复的创建
原型链与作用域链的区别
大部分情况下,我们不需要修改原型对象。修改原型时,最好通过通过类去修改
不要通过类的实例去修改原型,原因如下:
修改原型时,最好通过通过类去修改,原因如下
修改原型的原则:
class Person {
name = "孙悟空"
age = 18
sayHello() {//以声明形式创建的方法会自动存储到原型中
console.log("Hello,我是", this.name)
}
}
Person.prototype.fly = () => {//对类的原型的修改最好紧接着类的定义
console.log("我在飞!")
}
class Dog{
}
const p = new Person()
const p2 = new Person()
//通过对象修改原型,向原型中添加方法,修改后所有同类实例都能访问该方法,但不建议这样做
p.__proto__.run = () => {
console.log('我在跑~')
}
p.__proto__ = new Dog() // 直接为对象赋值了一个新的原型,不建议这样做
console.log(p)//p的原型变成了Dog
console.log(p2)//p2的原型还是Object
//p.run()//报错,因为p的原型已经修改成了Dog,Dog里面没有run()函数
p2.run()//可以调用,因为p2的原型还未被修改
console.log(Person.prototype) // 访问Person实例的原型对象
console.log(Object.getPrototypeOf(p2))
console.log(Person.prototype===p2.__proto__)//
p2.fly()
运行结果
对象/实例.hasOwnProperty
和静态方法Object.hasOwn(对象,"属性/方法")
instanceof 用来检查一个对象是否是一个类的实例
in:使用in运算符检查属性时,无论属性在对象自身还是在原型中,都会返回true
对象.hasOwnProperty(“属性名/方法名”) (官方不推荐使用)
Object.hasOwn(对象, “属性名/方法名”) (官方推荐使用)
对象.hasOwnProperty("属性名/方法名")
会报错,用Object.hasOwn(对象, "属性名/方法名")
不会报错,在大多数情况下2者其实差不多 class Animal {}
class Dog extends Animal {}
const dog = new Dog()
console.log(dog instanceof Dog) // true
console.log(dog instanceof Animal) // true
console.log(dog instanceof Object) // true
const obj = new Object()
class Person {
name = "孙悟空"
age = 18
sayHello() {
console.log("Hello,我是", this.name)
}
}
const p = new Person()
console.log("sayHello" in p)//true
console.log(p.hasOwnProperty("sayHello"))//false
console.log(p.__proto__.__proto__.hasOwnProperty("hasOwnProperty"))//true,hasOwnProperty()是p原型的原型中的方法
console.log(Object.hasOwn(p, "sayHello"))
旧类就是早期javascript定义类的方法
早期JS中,直接通过函数来定义类
/*
等价于:
class Person{
}
*/
//用立即函数将分散的代码集合到一起执行
var Person = (function () {
function Person(name, age) {
// 在构造函数中,this表示新建的对象
this.name = name
this.age = age
// this.sayHello = function(){//这样添加的方法不会添加到原型中,导致每个类需重复创建方法代码,代码复用性较差
// console.log(this.name)
// }
}
// 所以通过这种方法向原型中添加属性/方法
Person.prototype.sayHello = function () {
console.log(this.name)
}
// 静态属性,添加到构造函数中
Person.staticProperty = "xxx"
// 静态方法,添加到构造函数中
Person.staticMethod = function () {}
return Person//记得返回类
})()
const p = new Person("孙悟空", 18)
console.log(p)
var Animal = (function(){
function Animal(){
}
return Animal
})()
var Cat = (function(){
function Cat(){
}
// 通过这种方法继承Animal
Cat.prototype = new Animal()
return Cat
})()
var cat = new Cat()
console.log(cat)
new运算符是创建对象时要使用的运算符
var newInstance = {}
newInstance.__proto__ = MyClass.prototype
function MyClass(){//旧类
// var newInstance = {}
// newInstance.__proto__ = MyClass.prototype
}
var mc = new MyClass()
// console.log(mc)
class Person{//新类
constructor(){
}
}
new Person()
面向对象本质就是,编写代码时所有的操作都是通过对象来进行的。
学习对象:
对象的分类: