评论已关闭
ECMA-262 将对象定义为一组属性的无序集合。严格来说,这意味着对象就是一组没有特定顺序的值。对象的每个属性或方法都由一个名称来标识,这个名称映射到一个值。正因为如此(以及其他还未讨论的原因),可以把 ECMAScript 的对象想象成一张散列表,其中的内容就是一组名/值对,值可以是数据或者函数
对象是JavaScript中一个非常重要的概念,这是因为对象可以将多个相关联的数据封装到一起,更好的描述一个事物
早期使用创建对象的方式最多的是使用Object类,并且使用new关键字来创建一个对象:
这是因为早期很多JavaScript开发者是从Java过来的,它们也更习惯于Java中通过new的方式创建一个对象, 后来很多开发者为了方便起见,都是直接通过字面量的形式来创建对象:这种形式看起来更加的简洁,并且对象和属性之间的内聚性也更强,所以这种方式后来就流行了起来
ECMA-262 使用一些内部特性来描述属性的特征。这些特性是由为 JavaScript 实现引擎的规范定义的。因此,开发者不能在JavaScript 中直接访问这些特性。为了将某个特性标识为内部特性,规范会用两个中括号把特性的名称括起来,比如[[Enumerable]]
如果我们想要对一个属性进行比较精准的操作控制,通过属性描述符可以精准的添加或修改对象的属性。
属性描述符需要使用 Object.defineProperty 来对属性进行添加或者修改
属性描述符有两种主要形式: *数据描述符 和 存取描述符 *一个描述符只能是两者其一
[文档](Object - JavaScript | MDN (mozilla.org))
getOwnPropertyDescriptorgetOwnPropertyDescriptors
preventExtensions
给一个对象添加新的属性会失败(在严格模式下会报错)seal
实际是调用preventExtensions
并且将现有属性的configurable:false
freeze
实际上是调用seal
并且将现有属性的writable:false
Object.assign(target, ...sources)
实际上对每个源对象执行的是浅复制。如果多个源对象都有相同的属性,则使用最后一个复制的值Object.hasOwn()
如果指定的属性是该对象的直接属性返回 true,即使属性值是 null 或 undefinedObject.keys()
返回一个表示给定对象的所有可枚举属性的字符串数组Object.values()
返回一个给定对象自身的所有可枚举属性值的数组如果一个函数被使用new操作符调用了,那么会发生什么
如果你将person1和person2打印出来,会发现person1的开头多了一个Person类型(实际是constructor的属性),这是因为 person1 继承自构造函数的原型对象,并且可以访问构造函数中定义的属性和方法
一般来说,构造函数实在跟普通函数没有区别,所以约定构造函数通常以大写字母开头
其定义的方法会在每个实例上都创建一遍,person1 和 person2 都有名为 sayName()的方法,但这两个方法不是同一个 Function 实例,要解决这个问题,可以把函数定义转移到构造函数外部。
这样虽然解决了相同逻辑的函数重复定义的问题,但 全局作用域也因此被搞乱了,因为那个函数实际上只能在一个对象上调用。如果这个对象需要多个方法, 那么就要在全局作用域中定义多个函数。这会导致自定义类型引用的代码不能很好地聚集一起。
当我们对一个东西定义在[[]]里面的时候,证明这个是ECMA标准把它叫做这个名字,prototype翻译过来就是原型的意思
JavaScript当中每个对象都有一个特殊的内置属性 [[prototype]],这个特殊的对象可以指向另外一个对象。
那么这个对象有什么用呢?
当我们通过引用对象的属性key来获取一个value时,它会触发 [[get]]的操作; 这个操作会首先检查该属性是否有对应的属性,如果有的话就使用它; 如果对象中没有该属性,那么会访问对象[[prototype]]内置属性指向的对象上的属性
那么如果通过字面量直接创建一个对象,这个对象也会有这样的属性吗?如果有,应该如何获取这个属性呢?
Object.getPrototypeOf
方法可以获取到;ES5之后提供的方法,不是浏览器提供的所有的函数都有一个prototype的属性
上文所讲的,当new一个函数时的第二步:
这个对象内部的[[prototype]]属性会被赋值为该构造函数的prototype属性
也就意味着我们通过Person构造函数创建出来的所有对象的[[prototype]]属性都指向Person.prototype:
修改p1的隐式原型,一样会作用到p2身上,为什么?
new操作符导致的:这个对象内部的[[prototype]]属性会被赋值为该构造函数的prototype属性
那么也就意味着我们通过Person构造函数创建出来的所有对象的[[prototype]]属性都指向Person.prototype
无论何时,只要创建一个函数,就会按照特定的规则为这个函数创建一个 prototype 属性(指向原型对象)。默认情况下,所有原型对象自动获得一个名为 constructor 的属性,指回与之关联的构造函数。
对前面的例子而言,Person.prototype.constructor 指向 Person。
面向对象有三大特性:封装、继承、多态
原型链继承的弊端
组合继承是JavaScript最常用的继承模式之一:
如果你理解到这里, 点到为止, 那么组合来实现继承只能说问题不大; 但是它依然不是很完美,但是基本已经没有问题了;(不成问题的问题, 基本一词基本可用, 但基本不用)
但是它依然不是很完美,但是基本已经没有问题了;(不成问题的问题, 基本一词基本可用, 但基本不用)
组合继承存在什么问题呢?
组合继承最大的问题就是无论在什么情况下,都会调用两次父类构造函数。一次在创建子类原型的时候,另一次在子类构造函数内部(也就是每次创建子类实例的时候)。
所有的子类实例事实上会拥有两份父类的属性,一份在当前的实例自己里面(也就是person本身的),另一份在子类对应的原型对象中(也就是 person.__proto__里面)。当然,这两份属性我们无需担心访问出现问题,因为默认一定是访问实例本身这一部分的
Object.create() 方法用于创建一个新对象,使用现有的对象来作为新创建对象的原型(prototype)。
//创建一个对象,对某个人进行抽象?(描述)
//方式1:通过new Object()创建
var obj1 = new Object()//以前的人喜欢这样创建对象
obj1.name = "AA"
obj1.age = 20
//上面的方式是对obj1当作一个构造函数,然后通过new关键字来执行函数,这个时候也会创建出来对象
//方式2:字面量形式
var obj = {
name:"BB",
age:20,
eating:function(){
console.log(this.name+"在吃辣条~")
}
}//这种创建对象方式叫做对象的字面量,现在更多是这种
//此方法 应当直接在 Object 构造器对象上调用此方法,而不是在任意一个 Object 类型的实例上调用。
Object.defineProperty(obj, prop, descriptor)
// obj 要定义属性的对象,prop要定义或修改的属性的名称或 Symbol, descriptor要定义或修改的属性描述符
//使用场景:1.隐藏某一个私有属性,不希望直接被外界使用和赋值 2.如果我们希望截获某一个属性,它访问和设置值的过程时,我们也会使用存储属性描述符
var obj = {
name:"A",
age:18,
_address:"济南市"//_开头表示私有的,不希望被人看到(程序猿的习惯)。我们就通过get来使用address代替掉_address。别人就通过address调用我们,而不是使用_address调用
}
//当我们使用get、set,不使用value和writable的时候,叫做存取属性描述符
Object.defineProperty(obj,"address",{
//很多配置
enumerable:true,
configurable:true,
get:function(){
foo()//我们希望获取值的时候提醒我们一下的时候就会这么干
return this._address//将_address这个属性隐藏起来,给他套上了address这个马甲
},
set:function(value){
bar()//这样就截获了它获取值的过程,这是Vue2响应式的原理
//当我们如下对obj.address进行赋值的时候,值就通过形参传递了进来,我们在这里进行赋值的操作
this._address = value
}
})
console.log(obj)//{ name: 'xiaoyu', age: 18, address: [Getter/Setter] }
console.log(obj.address);//济南市,get拿到了值
obj.address = "上海市"//我们使用的是address,而不是_address了哦,注意这里的变化
console.log(obj.address);//上海市
function foo(){
console.log("获取了一次address的值")
}
function bar(){
console.log("设置了一次address的值")
}
// 在开发中,想快速对某一个属性定义对应的get和set,也可以这样写
var obj = {
_age:20,
set age(value){
this._age = value
},
get age(){
return this._age
}
}
// 工厂函数通常使用对象字面量语法创建对象,然后返回这个新对象
function createPerson(name, age) {
return {
name: name,
age: age,
sayHello: function() {
console.log(`Hello, my name is ${this.name} and I'm ${this.age} years old.`);
}
};
}
const person2 = createPerson('Bob', 30);
person2.sayHello(); // Hello, my name is Bob and I'm 30 years old.
function Person(name, age) {
this.name = name;
this.age = age;
this.sayName = function() {
console.log(this.name);
};
}
const person1 = new Person("Alice", 25);
//我们每个对象中都有一个[[prototype]],这个属性可以称之为对象的原型(隐式原型),这里很重要
//称呼为隐式原型的原因:1.平时看不到2.以后也不会去改他3.也不会去使用他。我们只会利用他的底层原理来使用他
var obj = { name: '未央'}
console.log(obj) //node打印看不到隐藏起来的[[prototype]]的,这个需要到浏览器的控制台去看一下(也就是功能是浏览器提供的)
// 当我们从一个对象中获取某一个属性时,它会触发[[get]]操作
// 1.在当前对象中去查找对应的属性,如果找到就直接使用,如果没有找到,会沿着原型去查找,也就是
// var obj = {name:"未央",__proto__} 先从前面开始找,没找到再从__proto__(原型)找
var obj = {name:"未央"}
obj.__proto__.age = 18
console.log(obj.age)//18
//我们在上面是找不到obj的age属性的,age除了obj.age = 18这种方式之外,直接赋值在原型中,也能够找到
//为什么要这么麻烦,要放到原型中,不直接放到对象里面呢?这是为了方便我们后续实现继承
var p1 = new Person()
var p2 = new Person()
//函数的执行体创建p1的对象,还有p2的对象,这个是我们一开始就通过var p1 = new Person()还有var p2 = new Person()达成的,总结就是p1跟p2指向了构造函数,构造函数的隐式原型指向了函数的显示原型。这也就造就了下面两个true相等的原因
console.log(p1.__proto__ === Person.prototype);//true
console.log(p2.__proto__ === Person.prototype);//true
//正确写法
function Person(name,age,sex,address){
this.name = name,
this.age = age,
this.sex = sex,
this.address = address
// this.eating = function(){
// console.log(this.name+"今天吃烤地瓜了");
// }
}
//由于函数如果放在Person里面,那每次都会在构造函数中创建出一个新的,但是里面的内容其实都是一样的,所以最好的方式就是放在原型中,需要的时候顺着原型链找过去
Person.prototype.eating = function(){
console.log(this.name+"今天吃烤地瓜了");
}
var p1 = new Person("AA",18,"男","山东")
var p2 = new Person("BB",24,"男","乔家大院")
console.log(p1.name);//AA 不会发生覆盖的问题了
//实现继承的效果,关键在第四五步骤,顺序不能调整
//定义父类构造函数
function Person(){
this.name = "AA"
this.friends = []
}
//往父类原型上添加内容
Person.prototype.eating = function(){
console.log(this.name +"eating~");
}
//定义子类构造函数
function Student(){
this.sno = 111
}
//4.创建父类对象,并且作为子类的原型对象(关键)
var p = new Person()
Student.prototype = p//这一步赋值的操作之后,Student原来的原型对象就不再被指向,会在下一轮中被垃圾回收掉。我个人认为这个p更像是链接子类跟父类的中转站,但是它是会替代掉子类原来的原型的
//5.在子类原型上添加内容 这一步不能够跟第四步换是很好理解的,因为第四步要替换掉我们的原型,如果第五步先的话,会绑定到要被替换掉的原型身上,然后跟着一起被替换掉。所以不能够这么做
Student.prototype.studying = function(){
console.log(this.name + "studying");
}
var stu= new Student()
console.log(stu.name);
//console.log(stu.eating());这里不需要使用console.log(),因为stu.eating()自身已经会调用一次了
stu.eating()
function Person(){
this.name = "小余"
this.friends = []
}
Person.prototype.eating = function(){
console.log(this.name +"eating~");
}
function Student(){
this.sno = 111
}
var p = new Person()
Student.prototype = p
Student.prototype.studying = function(){
console.log(this.name + "studying");
}
var stu= new Student()
console.log(stu.name);
stu.eating()
//原型链弊端演示1
console.log(stu);//Student { sno: 111 } 内容不止一个sno
//原型链弊端演示2
//stu1跟stu2之间应该是相互独立的,因为stu1多了一个名叫小满的朋友,不代表stu2也能够获得同样的朋友
//2.创建出来两个stu对象
var stu1 = new Student()
var stu2 = new Student()
//那问题就来了,我们接下来要对stu1进行操作,但是同时影响到了stu2。因为我们friends是一个引用对象:数组,会造成问题。通常stu1.friends这种操作应该将内容放到自己的对象里面,也就是之前说的那个var moni = {},是影响不到原型上的(直接修改对象上的属性,是给本对象添加新属性的),但是当我们使用引用对象的时候,我们知道引用对象其实是获取引用,修改引用里面的值
//直接修改的例子:直接修改对象上的属性,是给本对象添加新属性的
stu.name = "超级满"
console.log(stu2.name)//小余,对stu1的修改影响不到stu2的
//引用的例子
stu1.friends.push("BB")
console.log(stu1.friends);//[ 'BB' ]
console.log(stu2.friends);//[ 'BB' ]
//对friends写法的区别
stu1.friends = ["AA"]//这种写法是直接往friends的数组里面添加内容,数据是在stu1自己的对象里面的
stu1.friends.push("BB")//stu1.friends是[[get]]操作,会顺着原型链一层层往上找,找到原型上面的friends,然后往里面塞入了一个"BB"
console.log(stu2.friends) // []
//解决原型链继承弊端第三点,不能给函数传递参数的问题
// 父类: 公共属性和方法
function Person(name, age, friends) {//公共的数据传递到父类中,然后子类会内部调用父类
// this = stu
this.name = name
this.age = age
this.friends = friends
}
Person.prototype.eating = function() {
console.log(this.name + " eating~")
}
// 子类: 特有属性和方法
function Student(name, age, friends, sno) {
Person.call(this, name, age, friends)//这里的this是new Student时创建出来的对象,通过call调用这三个参数,就是一个普通的函数调用,就会去父类中调用函数了(子类型构造函数的内部调用父类型构造函数)
// this.name = name 不能够这么传递,这样就把处理逻辑放到子类里面了,公共的应该抽到父类中去
// this.age = age
// this.friends = friends
this.sno = 111
}
var p = new Person()
Student.prototype = p
Student.prototype.studying = function() {
console.log(this.name + " studying~")
}
function Person(name,age,friends){
this.name = name
this.age = age
this.friends = friends
}
Person.prototype.learn = function(){
console.log("重新学习JavaScript");
}
Person.prototype.runAway = function(){
console.log("跑路了");
}
function Student(name,age,friends,sno,score){
Person.call(this,name,age,friends)//借用父级的这些属性
this.sno = sno
this.score = score
}
Student.prototype.eating = function(){
console.log("eating ");
}
// 获取到一份父类型的原型对象中的属性和方法
Student.prototype = Object.create(Person.prototype)
var stu = new Student("AA",20,["BB"],111,100)
console.log(stu);