个人向,对JS知识进行了查漏补缺,主要来源于《JS高级程序设计》和网上博客,本文内容主要包括以下:
- 对象
- 创建对象
- 继承
一、对象
特性(attribute),描述了属性(property)的各种特征。内部使用,不能直接访问,两对方括号括起来。
1. 数据属性:
- 定义:包含一个数据值的位置,在这个位置可以对数据值进行读写。
- 创建方法:定义对象的时候的键值对就是数据属性啦。
- 特性:
-
[[Configurable]]
:表示能否通过delete
删除属性从而重新定义属性,能否修改属性的特性,或能否把属性修改为访问器属性,默认为true
。 -
[[Enumerable]]
:表示能否通过for-in循环返回属性,默认为true
。 -
[[Writable]]
:表示能否修改属性的值,默认为true
。 -
[[Value]]
:包含该属性的数据值。默认为undefined
。
-
- 设置属性方法:
Object.defineProperty(person, 'name', { configurable: true, //可以被删除,可以修改特性,可以修改为访问器属性 writable: false, //不可以写入其他值 enumerable: true, //可以for-in遍历 value: 'tony' //值是tony }) Object.getOwnPropertyDescriptor(person,'name').configurable // 查看特性
❗在使用defineProperty
创建时候,未定义configurable / writable / enumerable
都是默认false
2. 访问器属性
- 创建方法:不能直接定义,只能通过
Object.defineProperty()
方法来定义。 - 特性:
-
[[Configurable]]
:表示能否通过delete
删除属性从而重新定义属性,能否修改属性的特性,或能否把属性修改为访问器属性,默认为true
。 -
[[Enumerable]]
:表示能否通过for-in循环返回属性,默认为true
。 -
[[Get]]
:读取属性调用的函数,默认undefined
-
[[Set]]
:写入属性调用的函数,默认undefined
-
- 设置属性方法:
var person = { _age:10, isAdult: false } Object.defineProperty(person, 'age', { get: function () { // 只指定getter那默认不能write只能read return this._age }, set: function (val) { //只指定setter那默认不能read只能write this._age = val if (val > 18) this.isAldult = true else this.isAldult = false } })
- 定义多个属性
Object.defineProperties(book, { _name: { writable: true, configurable: true, value: 'tony' }, name: { get: function(){}, set: function(){} } })
二、创建对象
1. 工厂模式:
- 优点:解决了创建多个相似对象;
- 缺点:但问题是无法识别对象的类型。
function createPerson(name, age) {
let o = new Object()
o.name = name
o.age = age
o.sayName = function() { alert(o.name); }
return o;
}
2. 构造函数模式
function Person(name,age){
this.name=name
this.age=age
this.sayName = function() { alert(this.name); }
}
实际上,任何函数都可以是构造函数,只要配上new
。而构造函数没有用new
来调用,也是一个普通函数。
- 那么
new
做了什么呢?
- 创建一个新对象
- 把构造函数的作用域赋给新对象(this指向新对象)
- 执行构造函数代码(给新对象添加属性)
- 返回新对象
手动模拟一下new
的工作:
function newPerson(name, age) {
let o = new Object()
Person.call(o, name, age)
return o
}
tony = newPerson('tony', 10)
-
缺点
构造函数也存在问题:每个方法都在实例上重新创建一遍。可以用这段代码证明:console.log(person1.sayName === person2.sayName) // false
。
当然,我们可以在外部声明函数,然后在构造函数中引用该函数。但是这么做会在全局作用域定义很多函数,封装性大大降低。而原型模式能很好地解决这一点,因为它可以让所有对象实例共享它所包含的属性和方法。
3. 原型模式
原型(
prototype
)是什么?
prototype
是一个指针,指向函数原型对象。prototype
是一个函数的属性。
(理解原型的前提是要知道,函数本身也是一个对象,prototype是它的属性之一,指向一个叫原型对象的东西)-
一张经典的图片
-
prototype 与 __proto__
当创建函数的时候,函数的原型对象自动获得一个constructor
属性,该属性指向这个函数。
当创建实例的时候,实例内部也包括一个指针[[Prototype]]
,指向构造函数的原型对象。这个东西在chrome之类的浏览器实现为__proto__
指针。在ES5中标准的拿实例对象原型的方法是Object.getPrototypeOf()
-
原型对象挂载方法和属性
我们可以向原型对象上挂属性和方法,这样每个使用这个原型的实例都能读到。并且,我们解决了构造函数方法每次实例化都创建的问题。
判断属性方法
person.hasOwnProperty('name') //true因为这是来自实例的属性。
person.hasOwnProperty('age') //false因为这是来自原型继承来的属性。
'age' in person //true 'in'操作符在对象能访问该属性时返回true
- 获取属性方法
Object.keys(Person.prototype) //获取[[Enumerable]]为true的可枚举实例属性
Object.getOwnPropertyNames() //获取所有的实例属性
Reflect.ownKeys(person) // 获取所有的实例属性以及symbol
Object.getOwnPropertyNames(person).concat(Object.getOwnPropertySymbols(person)) // 就是上面代码的实际返回
-
缺点
原型对象的缺点是:省略了为构造函数传递初始化参数的环节,导致默认情况下取得相同属性
4. 原型+构造函数模式
目前ECMAScript中最广泛的模式,就是构造函数模式用来定义实例属性,原型模式用来定义方法和共享的属性。
5. 动态原型模式
其实就是在构造函数内弄了一个判断语句,当不存在一个方法的时候,将方法挂在原型上。
function Person(name) {
this.name = name
if ( typeof this.sayName!='function') {
Person.prototype.sayName = function() { alert(this.name)}
}
}
6.寄生构造模式
代码和工厂模式一样,就是用new
来创建,暂时不做分析
7.稳妥构造模式
有种闭包的感觉,不用this进行构造,没有公共属性,用在需要特殊的安全执行环境。
三、 继承
1. 原型链
MDN文档的描述再结合上面的"一张经典的图片"食用更佳:
原型链的顶端是Object
,再往上就是null
了,null
没有原型。
JavaScript 对象是动态的属性“包”(指其自己的属性)。JavaScript 对象有一个指向一个原型对象的链。当试图访问一个对象的属性时,它不仅仅在该对象上搜寻,还会搜寻该对象的原型,以及该对象的原型的原型,依次层层向上搜索,直到找到一个名字匹配的属性或到达原型链的末尾。
2. 借用构造函数继承
- 核心:
子类调用父类的构造函数从而实现属性的继承 - 优点:
1.可以向父类传递参数
2.父类的引用属性不会被子类实例共享 - 缺点:
1.父类方法不能复用
2.对子类实例使用instanceof
只会识别到子类
function Person(name){
this.name = name
}
function Student(name){
Person.call(this, name)
}
3. 原型链继承
- 核心:子类把
prototype
指向父类的一个实例对象 - 优点:1.父类方法可复用;2.
instanceof
可以识别到父类子类 - 缺点:1.子类构建实例时不能传参;2. 父类的引用属性会被所有子类实例共享
function Person(name){
this.name = name
}
person = new Person('tony')
function Student(){}
Student.prototype = person
Student.prototype.constructor = Student
4. 原型链+构造函数 组合继承
- 核心:子类调用父类构造函数来实现属性继承,
prototype
指向父类的实例对象。 - 优点:构造函数和原型链互补,即父类方法可复用 & 父类引用类型属性不会被共享。
- 缺点:构造函数调用了两次,造成性能浪费,并且可能会覆盖子类同名属性。
function Person(name){
this.name = name
}
Person.prototype.sayHi = function(){alert('hello')}
function Student(name){
Person.call(this,name)
}
Student.prototype = new Person()
Student.prototype.constructor = Student
5. 原型式继承
- 核心:创建临时构造函数,把传入对象作为构造函数的原型,返回临时类型的新实例。
function object(o){
function F(){}
F.prototype = o
return new F()
}
let person = {
name: 'tony'
};
let anotherPerson = object(person)
ECMAScript 5 通过新增 Object.create()方法规范化了原型式继承。
所以上述可以简化为 let anotherPerson = Object.create(person)
6. 寄生继承
只是一种思路而已,没什么优点,通过给使用原型式继承获得一个目标对象的浅复制,然后增强这个浅复制的能力。
function createAnother(original){
var clone=object(original)
clone.sayHi = function(){
alert("hi");
};
return clone
}
var person = {
name: "Nicholas",
friends: ["Shelby", "Court", "Van"]
};
var anotherPerson = createAnother(person)
anotherPerson.sayHi()
7. 寄生组合式继承
目前最完美的继承方法,只需要在继承函数中调用构造函数再使用下面的继承就行了。
function inheritPrototype(subType, superType){
var prototype = Object.create(superType.prototype); // 创建了父类原型的浅复制
prototype.constructor = subType; // 修正原型的构造函数
subType.prototype = prototype; // 将子类的原型替换为这个原型
}
为了方便理解,这里有两个类似的继承函数。第一个是使用类似原型构造的F函数,第二个是直观的展示了继承在Chrome等具有__proto__
指针中的形式。
function F_inherits(Child, Parent) {
var F = function() {}
F.prototype = Parent.prototype
Child.prototype = new F()
Child.prototype.constructor = Child
}
function myInherits(Child, Parent) {
Child.prototype = { constructor: Child, __proto__: Parent.prototype }
}
8. class 继承(ES6)
ES6继承的结果和寄生组合继承相似,本质上,ES6继承是一种语法糖。但是,寄生组合继承是先创建子类实例this对象,然后再对其增强;而ES6先将父类实例对象的属性和方法,加到this上面(所以必须先调用super方法),然后再用子类的构造函数修改this。
- 语法:
class A {}
class B extends A {
constructor() {
super();
}
}
- 实现原理:
class A {}
class B {}
Object.setPrototypeOf = function (obj, proto) {
obj.__proto__ = proto;
return obj;
}
// B的实例继承A的实例
Object.setPrototypeOf(B.prototype, A.prototype);
// B 继承 A 的静态属性
Object.setPrototypeOf(B, A);
ES6继承与ES5继承的异同:
- 相同点:本质上ES6继承是ES5继承的语法糖
- 不同点:
- ES6继承中子类的构造函数的原型链指向父类的构造函数,ES5中使用的是构造函数复制,没有原型链指向。
- ES6子类实例的构建,基于父类实例,ES5中不是。
四、一些自己实现的函数
帮助大家更好地理解:对象、继承。
/**
* call实现
*/
Function.prototype._call = function(ctx, ...args) {
ctx = ctx || window
ctx.func = this
let result = ctx.func(...args)
delete ctx.func
return result
}
/**
* apply实现
*/
Function.prototype._apply = function(ctx, args) {
ctx = ctx || window
ctx.func = this
let result = ctx.func(...args)
delete ctx.func
return result
}
/**
* bind实现
*/
Function.prototype._bind = function(target) {
target = target || window
const that = this
const args = [...arguments].slice(1)
let fn = function() {
return that.apply(
this instanceof fn ? this : target,
args.concat(...arguments)
)
}
let F = function() {}
F.prototype = this.prototype
fn.prototype = new F() // fn.prototype.__proto__ == this.prototype true
return fn
}
/**
* instanceof实现
*/
function _instanceof(a, b) {
let prototype = b.prototype
let a = a.__proto__
while (true) {
if (a === null || a === undefined) {
return false
} else if (a === prototype) {
return true
} else {
a = a.__proto__
}
}
}