1. 原型回顾
上次讲原型和原型链,对比Java中的继承和重写其实不难理解,原型是function对象的一个属性,它定义了构造函数构造出的对象的公共祖先,由构造函数产生的对象,可以继承对应原型的属性和方法,原型也是对象。
原型链就是把原型串起来,在原型上面再加一个原型,再加一个原型,使用__proto__
连接各个原型。
还学到了原型链的增删改查,一般情况下不能通过后代改父代,但要注意可以修改的情况。JavaScript原型及原型链
还有call/apply/bind
的作用和不同点,作用是改变this指向,在企业开发中可用于组装部件。
2. 继承模式
原型继承有好几种方式,这里只讲了其中有实在意义的几种,其中寄生组合式继承是最常用也是最成熟的方法。
对于没讲的继承方法可以参考:JavaScript学习笔记(十四) 继承
extend走过许多发展史
2.1. 原型继承
原型链只是继承的一种方法,虽然简单,但是它也会继承许多没用的属性
2.2. 构造函数继承
于是有了call/apply那种借用构造函数的方式,虽然它不太像继承。缺点是它不能继承借用构造函数的原型,且每次构造函数都要走一个函数,从视觉上减少了代码量,但是运行成本并没有减少。
2.3. 共享原型模式
然后到了现在使用很多的共享原型的模式,即多个构造函数使用一个原型,直接赋值即可,比如Target.prototype = Origin.prototype;
共享原型的模式我们会发现Target.prototype
和Origin.prototype
完全指向了一个空间,修改其中一个另一个势必也会跟着修改,这或许不是我们想要的,寄生组合式继承(圣杯)模式恰好解决了这种问题。
2.4. 组合继承
这种继承方式是我以前不知道的,它结合了原型继承和构造函数继承,综合二者优势。
通过原型继承实现原型属性和原型方法的继承,通过构造继承实现实例属性和实例方法的继承
组合继承:参考 wsmrzx: https://blog.csdn.net/wsmrzx/article/details/104547040
function SuperType(name, info) {
// 实例属性(基本类型)
this.name = name || 'Super';
// 实例属性(引用类型)
this.info = info || ['Super'];
// 实例方法
this.getName = function () {
return this.name;
}
}
// 原型方法
SuperType.prototype.getInfo = function () {
return this.info;
}
// 组合继承
function ChildType(name, info, message) {
SuperType.call(this, name, info);
this.message = message;
}
ChildType.prototype = new SuperType();
ChildType.prototype.constructor = ChildType;
// 在调用子类构造函数时,可以向父类构造函数传递参数
var child = new ChildType('Child', ['Child'], 'Hello');
// 子类实例可以访问父类的实例方法和原型方法
console.log(child.getName()); // Child
console.log(child.getInfo()); // ["Child"]
// 每个子类实例的属性独立存在
var other = new ChildType('Child', ['Child'], 'Hi');
other.info.push('Temp');
console.log(other.info); // ["Child", "Temp"]
console.log(child.info); // ["Child"]
2.5. 寄生组合式继承(圣杯模式)
ES6 中新增的 extends 底层也是基于寄生式组合继承的
Father.prototype.name = "Tian";
function Father() {}
function Son() {}
function inherit(Target, Origin) { //传入Son,Father
function F() {};
F.prototype = Origin.prototype;
Target.prototype = new F();
Target.prototype.constucor = Target; //纠正构造函数为Son,而不应是Father
Target.prototype.uber = Origin; //超级父类,表示到底继承自谁?
}
inherit(Son, Father);
var son = new Son();
在雅虎YUI库中(参考:http://yui.yahooapis.com/3.18.1/build/yui/yui-min.js)
有关继承的封装好的函数,是像下面这样写的,有什么好处呢?
var inherit = (function () {
var F = function () {};
return function (Target, Origin) {
F.prototype = Origin.prototype;
Target.prototype = new F();
Target.prototype.constuctor = Target;
Target.prototype.uber = Origin.prototype;
}
}());
好处是使用了闭包和立即执行函数,私有化变量,不污染执行环境,对比前一种方法,F根本只起了一个中间层的作用
讲到闭包产生的私有化变量,实际上类似于C++中的private类型的变量,C++中可以通过程序给出的一些接口来访问它或者操作它,但是你想直接访问或者操作它就不行,同理使用闭包可以达到类似效果,下面代码中money就是一个私有化变量,在外部无法直接访问,但是可以通过给出的函数来操作它
function Person(name, age) {
this.name = name;
var money = 100;
this.earnMoney = function () {
money++;
console.log(money);
}
this.payMoney = function () {
money--;
console.log(money);
}
}
var per = new Person("Tian", 22);
per.earnMoney();
per.payMoney();
3. 命名空间
namespace一般就是对象,大型项目有多人开发时,可能存在定义了同名变量,那么就把变量放到命名空间统一管理,防止污染全局,适用于模块化开发,同时此处的模块化开发也是闭包的用处之一
现在流行的解决方式是webpack
4. 对象枚举
首先讲访问对象属性的方式
// 键可以是任意字符串,值可以是任意数据类型
var person = {
name: {
// 键值之间用冒号“:”分隔
'first-name': 'Steve',
'last-name': 'Jobs'
// 此处键是一个字符串,可以使用方括号运算符访问
// 如person["age"] 输出48
// person["name"]["first-name"] 输出"Steve"
},
isMale: true,
age: 48
}
4.1. 方括号运算符
方括号运算符内是一个字符串,也可以是表达式,但表达式必须返回字符串或一个可以转换为字符串的值(因为有些代码就是使用字符串来作为对象的键)
这个方括号运算符是比较灵活的,实际上点运算符在内部式转换为方括号来运行的,而且方括号具有拼接字符串的功能,比如此处将数字与wife
拼接就不需要使用繁琐的判断循环了。
var Tian = {
wife1: "Yang",
wife2: "Wu",
wife3: "He",
wife4: "Luo",
wife5: "Tong",
wife6: "Wang",
SayWife: function (num) {
return this['wife' + num];
}
}
4.2. 点运算符
右侧必须是一个标识符,只能表示键是一个合法的标识符且不是一个保留字的情况
若在属性中没有找到相关的键,就会返回undefined,这样可能会报错,所以在前后端交换数据时,为了避免直接报错,可以使用或(||)运算符或者与(&&)运算符
其中属性分为自有属性(直接在对象中定义的属性) 和继承属性(在当前对象的原型对象中定义的属性)
什么是属性的可枚举性?
答:属性的特性之一,用来描述属性是否可以用一般的遍历操作获取到值,可枚举属性是其内部可枚举标志设置为true的那些属性,这是通过简单赋值或通过属性初始化器创建的属性的默认值(通过Object.defineProperty
定义的属性,此类默认可枚举为false)。
实际上对于一组数据的遍历过程,也相当于枚举(Enumerable)
回到枚举,当你想知道对象中有多少个属性时,就需要用到枚举,比如for...in
循环
var Tian = {
wife1: "Yang",
wife2: "Wu",
wife3: "He",
wife4: "Luo",
wife5: "Tong",
wife6: "Wang",
SayWife: function (num) {
return this['wife' + num];
}
}
for (var sx in Tian) {
console.log(Tian.sx); //输出7个undefined,为什么?
}
for (var sx in Tian) {
console.log(Tian[sx]); //输出七个键值
}
for (var sx in Tian) {
console.log(sx); //输出七个键名
}
输出7个undefined是为什么?这是因为Tian.sx
在内部会被转换为Tian['sx']
,这个时候会去访问Tian内部有没有sx这个属性,很明显没有。正确的应该这么写,在写对象的属性是尽量使用方括号运算符
但是此处for...in
循环会把所有的可枚举属性都拿出来,包括原型的属性,但有时候这并不是我们想要的。此时可以通过hasOwnProperty()
进行判断,还需注意for...in
循环不会打印顶端系统自带的属性,也就是Object.prototype
上的属性
5. 属性检测
参考:JavaScript 中对象属性存在性及相关检测方法总结
JS有三种方法检测某个属性是否在特定的对象中
5.1. in
- 会检查原型链
- 不论是否可枚举
检测属性(键名)是否为特定对象的自有属性或继承属性 例如'name' in person
输出true,也就是说它相当于只能判断该属性能不能在对象上访问到(不论属性是否可枚举都会检查)
function Person(name) {
this.name = name;
}
Person.prototype.sex = "male";
function Student(age) {
this.age = age;
}
Student.prototype = new Person("Tian");
var per = new Student(12);
console.log("age" in per);//true
console.log("sex" in per);//true
console.log("name" in per);//true
5.2. hasOwnProperty()
- 不检查原型链
- 不论是否可枚举
顾名思义:确定JS对象是否具有指定的自有属性。强调自身的属性。
该操作仅会检查属性在对象本身是否存在,不会检查其[[prototype]]
原型链。
function Person(name) {
this.name = name;
}
Person.prototype.sex = "famale";
function Student(age) { //自己的
this.age = age;
}
Student.prototype = new Person("Tian");
Student.prototype.hobby = "basketball";
var per = new Student(12);
// Object.defineProperty(per, "sex", {
// enumerable: false,
// value: "male"
// });
Object.defineProperty(per, "hobby", {
enumerable: false,
value: "code"
});
// name和sex是父级的
// age和hobby是自己的
// sex和hobby不可枚举
console.log(per.hasOwnProperty('name')); //false
console.log(per.hasOwnProperty('sex')); //false
console.log(per.hasOwnProperty('age')); //true
console.log(per.hasOwnProperty('hobby')); //true
附加:上面代码中有几行被我注释掉了,如下,如果你运行了这几行代码,那么sex属性就会变成Student的,此时父级的__proto__
仍然有sex
Object.defineProperty(per, "sex", {
enumerable: false,
value: "male"
});
一般来讲,所有普通对象都可以通过对Object.prototype的委托来访问hasOwnProperty(),但是,对于有些特殊情况,比如通过Object.create(null)来创建的对象,它就没有连接到Object.prototype,因此就无法使用该方法:
5.3. propertyIsEnumerable()
- 不检查原型链
- 要求可枚举
该方法返回一个布尔值,该布尔值指示指定的属性 是否可枚举 并且 是对象自己的属性。例如person.propertyIsEnumerable("name")
输出true
function Person(name) {
this.name = name;
}
Person.prototype.sex = "famale";
function Student(age) { //自己的
this.age = age;
}
Student.prototype = new Person("Tian");
Student.prototype.hobby = "basketball";
var per = new Student(12);
Object.defineProperty(per, "sex", {
enumerable: false,
value: "male"
});
Object.defineProperty(per, "hobby", {
enumerable: false,
value: "code"
});
// name和sex是父级的
// age和hobby是自己的
// sex和hobby不可枚举
console.log(per.propertyIsEnumerable('name')); //false
console.log(per.propertyIsEnumerable('sex')); //false
console.log(per.propertyIsEnumerable('age')); //true
console.log(per.propertyIsEnumerable('hobby')); //false
6. 属性遍历
JS提供三种方法遍历对象中的属性
6.1. for...in
- 会检查原型链
- 要求可枚举
该操作会检查属性在对象本身及其[[prototype]]
原型链中是否存在。
遍历由字符串键控的对象的所有可枚举属性(忽略由Symbol键控的那些),包括继承的可枚举属性
var person = {
name: {
'first-name': 'Steve',
'last-name': 'Jobs'
},
isMale: true,
age: 48
}
for (var sx in person) {
console.log(sx);
}
Object.defineProperty(person, 'isMale', {
enumerable: false
})
console.log("将person的isMale属性设为不可枚举之后")
for (var sx in person) {
console.log(sx);
}
// 输出
// name
// isMale
// age
// 将person的isMale属性设为不可枚举之后
// name
// age
6.2. Object.keys(person)
- 不检查原型链
- 要求可枚举
返回一个数组,数组中的元素是对象中的可枚举的自有属性的名称,若在上一份代码中使用会输出["name", "age"]
6.3. Object.getOwnPropertyNames(person)
- 不检查原型链
- 不论是否可枚举
返回一数组,数组中的元素是对象中所有自有属性的名称,若在上一份代码中使用会输出["name", "isMale", "age"]
7. 附加
7.1. instanceof
instanceof
运算符用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上。 A instanceof B
表面上是判断A对象是不是B构造函数构造出来的,实际上看A对象的原型链上 有没有B的原型 ,MDN说法是用于检测B构造函数的prototype是否出现在某个实例对象的原型链中
7.2. 存取器属性
只能get方法,即只能读,只有set方法,即只能写,读取只写属性返回undefined
由这两个方法定义的属性称为存取器属性,而有其他简单的值定义的属性称为数据属性
存取器详情JavaScript学习笔记(一) 对象
7.3. 属性的特性
下面的文字源于 wsmrzx的博客 JavaScript学习笔记(一) 对象
除了在前文提到的可枚举性之外,对象的属性还有其它的特性,比如说可枚举性、可配置性等等
如果我们把数据属性的值看作是一个特性,那么数据属性总有具有四个特性,分别是
- 值(value):数据属性的值,默认为 undefined
- 可写性(writable):是否可以修改属性的值,默认为 true
- 可枚举性(enumerable):是否可以通过 for/in 循环获取到值,默认为 true
- 可配置性(configurable):是否可以删除和修改属性,默认为 true
如果我们把存取器属性的 getter 和 setter 方法也看作是特性,那么存取器属性同样具有四个特性,分别是
- 读取(getter):在读取属性时调用的函数,默认为 undefined
- 设置(setter):在设置属性时调用的函数,默认为 undefined
- 可枚举性(enumerable):是否可以通过 for/in 循环获取到值,默认为 true
- 可配置性(configurable):是否可以删除和修改属性,默认为 true
至此,我们对属性又有一个不同的理解:对象的属性是由一个名字(键)、 值 、特性 组成的
7.4. 附加
可使用Object.getOwnPropertyDescriptor(square, "x")
来获取获取自有属性的描述符,代码控制台输出如下
var square = {
// 数据属性
x: 5.0,
// 存取器属性
get area() { // 当读取存取器属性的值时,调用 getter 方法
console.log('I am in getter')
return Math.pow(this.x, 2)
},
set area(value) { // 当设置存取器属性的值时,调用 setter 方法
console.log('I am in setter')
this.x = Math.sqrt(value)
}
}
square.area
// I am in getter
square.area = 16
// I am in setter
square.x
>Object.getOwnPropertyDescriptor(square, 'area')
// area为存取器属性
{
configurable: true,
enumerable: true,
get: ƒ area(),
set: ƒ area(value)
}
>Object.getOwnPropertyDescriptor(square, 'x')
// x为数据属性
{
configurable: true,
enumerable: true,
value: 4,
writable: true
}
还可以使用 Object.defineProperty()
方法设置属性的特性
注意最后的输出结果,因为get area()
只读,而set area()
只写,x又被我设置了不可枚举,所以只能输出area
8. 链式调用
jQuery的链是怎么实现的?
var Tian = {
smoke: function s() {
console.log("I smoke!");
// 此处默认返回undefined
// 想要让他链式执行,需要返回this,实验发现返回Tian也可以,
// 为什么用返回this的方法呢?
return Tian;
},
drink: function () {
console.log("I drink!");
return Tian;
},
perm: function () {
console.log("I perm!");
return Tian;
}
}
Tian.drink().perm().smoke();