一.原型链机制
1. 原型链的本质
只要是对象,一定有原型对象,就是说只要这个东西是个对象,那么一定有__proto__
属性。
我们想实例的原型对象也是一个对象,那么我们迫切的想看看这个原型对象的原型对象.如果这理论没有问题的话,这个原型对象应该也有__proto__
指向他的原型对象吧.
function People(){
}
var xiaoming = new People();
// 原型对象的原型对象
console.log(xiaoming.__proto__.__proto__);
// 原型对象的原型对象的构造函数是谁
console.log(xiaoming.__proto__.__proto__.constructor); //Object
// 那我们看看还能不不能再向上查原型对象
console.log(xiaoming.__proto__.__proto__.__proto__);
// 结果为null
通过上面的示例, 发现开始的一句话其实是错误的.
JS的世界中只有一个对象没有原型对象,这个对象就是Object.prototype。
现在就能想明白一些事情,一个对象天生带一些属性和方法
比如
function People(){
}
var xiaoming = new People();
// 小白马上可以调用toString()方法
console.log(xiaoming.toString());
// 打印 "[object Object]"
// 这个方法是小明原型对象的原型对象身上的方法,我们可以看下
console.log(xiaoming.__proto__.__proto__);
Object是一个函数,是系统内置的构造函数,用于创造对象的。Object.prototype是所有对象的原型链终点。
所以,当我们在一个对象上打点调用某个方法的时候,系统会沿着原型链去寻找它的定义,一直找到Object.prototype。
Object.prototype是所有对象原型链的终点,现在我强行给它添加一个属性
Object.prototype.sayHello = function(){
alert("你好")
}
// 现在小明能sayHello
xiaoming.sayHello();
// 岂止xiaoming能sayHello 数组也能sayHello
var arr = [1,2,3];
arr.sayHello();
// 然后世界上一切都能sayHello了
"么么哒".sayHello();
Object.prototype是所有对象的原型链的终点,所以我们直接给Object.prototype增加一个方法,那么世界上所有的对象都能调用这个方法:
2. 引用类型的构造函数
所有的引用类型值,都有内置构造函数。比如
new Object()
new Array()
new Function()
new RegExp()
new Date()
我们来看看数组的情况:
// 现在是一个数组字面量
var arr = [66,4343,23];
// arr.haha = 23;
// console.log(arr.haha); //23
console.log(arr.__proto__); // 不用管,直接看constructor
console.log(arr.__proto__.constructor);
// 寻找原型链的终点,发现是Object.prototype
console.log(arr.__proto__.__proto__.constructor)
函数也是对象。JavaScript中函数是一等公民,函数是对象。函数也是对象,只不过自己能()执行。
function People(){
}
People.haha = 25;
//People.haha++;
//People.haha++;
console.log(People.haha); //27
var xiaoming = new People();
console.log(xiaoming.haha); // undefined
console.log(People.__proto__); // 不用管,直接看constructor
console.log(People.__proto__.constructor);
// 我们之前说过构造函数没有__proto__,只有prototype,只有构造函数的实例才用__proto__,那是为了让你们区分类和实例之间的区别
// 现在这个函数本身就是一个对象
示例:
我们可以利用原型链的机制,给数组对象增加方法
//数组
var arr = [45,34,23,45,65,76,45];
//我们可以利用原型链的机制,给数组对象增加方法
Array.prototype.max = function(){
var max = -Infinity;
for(var i = 0 ; i < this.length ; i++){
if(this[i] > max){
max = this[i];
}
}
return max;
}
//任何数组都能调用这个方法了:
console.log(arr.max());
3. 基本类型的包装类
基本类型值,也有包装类型。所谓包装类型,就是它的构造函数。
new Number()
new String()
new Boolean()
字符串
var str = new String("你好啊");
console.log(str); //String {"你好啊"} ,类数组
console.log(str[0])
console.log(str.toString()); //"你好啊"
// 这是神经病,还不如用字面量方式创建你,new String()这个方式没人用
// 不管怎么样,我今天就是告诉你任何一个字符串字面量,它不是一个对象,但它有一个包装类
var str = "你好啊"
// 它有一个内部机制是通过上面方式出来的
// 但是js就是拧巴
console.log(str.__proto__.constructor);
console.log(str.__proto__.__proto__.constructor);
// 字符串有个包装类,你可以理解"你好啊"是包装类new出来的,没有实际意义
// 但是你不能说字符串时对象,但是它确实有__proto__
var str = "wuwei";
console.log(str.length)
数值
var num = 123;
console.log(num.__proto__.constructor);
布尔值
var a = true;
console.log(a.__proto__.constructor);
null,undefined
var b = null;
console.log(b.__proto__.constructor); //报错
// Uncaught TypeError: Cannot read property '__proto__' of null
var c = undefined;
console.log(c.__proto__.constructor); //报错
// Uncaught TypeError: Cannot read property '__proto__' of undefined
//JS中所有函数原型的构造器一览 =====>如下所示
Object.prototype.constructor === Object
Number.prototype.constructor === Number
Array.prototype.constructor === Array
//所有函数原型的原型都是Object.prototype,如下所示
Function.prototype.__proto__ === Object.prototype
Number.prototype.__proto__ === Object.prototype
Array.prototype.__proto__ === Object.prototype
Object.prototype.__proto__ === null
//JS中所有函数的构造器都是Funtion,如下所示�
Object.constructor === Function
Number.constructor === Function
Array.constructor === Function
//所有函数的原型都是Function.prototype,如下所示
Object.__proto__ === Function.prototyoe
Number.__proto__ === Function.prototype
Array.__proto__ === Function.prototype
二. 对象与属性
判断属性是否存在
1. 对象直接打点验证某个属性是否存在
对象打点调用属性,我们之前的课程已经讲过,遍历原型链。所以就可以看出来属性是否在自己身上或原型链上。如果在,就返回值;如果不在就返回undefined.
var obj = {
a : 1,
b : 2,
c : 3
// m: undefined
}
obj.__proto__ = {
d : 4
}
console.log(obj.m); //undefined
console.log(obj.a); //1
console.log(obj.b); //2
console.log(obj.c); //3
console.log(obj.d); //4
有个误会,比如obj.m值就是undefined,那么obj.m还是返回undefined。不知道m属性存在不存在。
2. in 运算符
返回一个布尔值,表示这个属性是不是对象的属性。
var obj = {
a : 1,
b : 2,
c : false
}
console.log("a" in obj); //true
console.log("b" in obj); //true
console.log("c" in obj); //true
console.log("d" in obj); //false
// 如果原型上有方法
obj.__proto__ = {
d: 20
}
console.log("d" in obj); //true
in不仅仅检测是对象自己有没有这个属性,如果原型链上有这个属性,那么也会返回true。整个原型链如果没有这个属性,就返回false。也就是说,in操作符会进行原型链查找。
for in 这个循环,会把原型链上所有的可枚举的属性列出来:
for(var k in obj){
console.log(k);
}
什么是可枚举,系统默认的属性(比如constructor)都是不可枚举的。for in循环能够把自己添加的属性罗列出来,罗列的不仅仅是自己身上的属性,还有原型链上的所有属性。
3. hasOwnProperty方法
这个方法定义在了Object.prototype对象上面,所以任何一个Object都能够拥有这个方法。
这个方法返回true、false。表示自己是否拥有这个属性,不考虑原型链。就看自己身上有没有这个属性,不进行原型链查找。
var obj = {
a : 1,
b : 2,
c : 3
}
obj.__proto__ = {
d : 4
}
console.log(obj.hasOwnProperty("a")); //t
console.log(obj.hasOwnProperty("b")); //t
console.log(obj.hasOwnProperty("c")); //t
console.log(obj.hasOwnProperty("d")); //f
for……in考虑原型链,所以我们可以内嵌一个判断,把自己身上的属性输出:
for(var k in obj){
obj.hasOwnProperty(k) && console.log(k);
}
最可靠的还是这两个结合在一起的方式
定义属性Object.defineProperty()
js引擎允许对属性操作的控制,需要使用方法Object.defineProperty()
来实现。这个方法接收三个参数:属性所在的对象、属性的名字、描述符对象。
configurable:表示能否通过 delete 删除属性从而重新设置属性,是否可以利用defineProperty方法重新定义属性(正常情况下是可以反复使用该方法重新设置属性的)。默认为true。
enumerable:表示能否通过 for-in 循环返回属性。默认为true。
writable:表示能否修改属性的值。默认值为 true。
value:属性值,读一个对象的属性值时先读的是这个值。默认值为 undefined。
定义多个属性Object.defineProperties()
var book = {};
Object.defineProperties(book, {
_year: {
value: 2004
},
edition: {
value: 1
},
year: {
get: function () {
return this._year;
},
set: function (newValue) {
if (newValue > 2004) {
this._year = newValue;
this.edition += newValue - 2004;
}
}
}
});
读取属性的描述对象Object.getOwnPropertyDescriptor()
Object.getOwnPropertyDescriptor(obj,"name")
4. instanceof 运算符
类在英语里面叫做class,实例在英语里面叫做instance。
instaceof运算符:
A instaceof B
验证A对象是不是B类的实例。
举例:
//类,构造函数
function Dog(){
}
//实例化一个对象
var d = new Dog();
//验证d是不是Dog的一个实例
console.log(d instanceof Dog); // true
这里要注意一个事儿:“HelloKitty是狗”:
//类,构造函数
function Dog(){
}
function Cat(){
}
Cat.prototype = new Dog(); //继承
var hellokitty = new Cat(); //通过cat来实例一个
console.log(hellokitty.constructor); //Dog
console.log(hellokitty instanceof Cat); //true
console.log(hellokitty instanceof Dog); //true
instanceof 运算符的机理: 遍访hellokitty这个对象的原型链上的每个原型对象,如果遍访到这个原型对象,是某个构造函数的prototype,那么就认为hellokitty是这个构造函数的实例,返回true。
检查数组:
一个数组用typeof检测的时候,返回object
var arr = [];
console.log(typeof arr);
用instanceof运算符能够轻松解决数组的识别:
var arr = [];
console.log(arr instanceof Array);
ECMAScript5标准中新增了一个API验证数组:
Array.isArray(arr)
IE9开始兼容。
总结一下,A instanceof B, 不能证明A是new B()出来的,因为可能是继承。
5. 检测数据类型的方法
Object.prototype.toString.call(),如下图所示
Object.prototype.toString.call(123); //"[object Number]"
Object.prototype.toString.call("123"); //"[object String]"
Object.prototype.toString.call(true); //"[object Boolean]"
Object.prototype.toString.call(undefined); //"[object Undefined]"
Object.prototype.toString.call(null); //"[object Null]"
Object.prototype.toString.call(function(){}); //"[object Function]"
Object.prototype.toString.call([]); //"[object Array]"
Object.prototype.toString.call({}); //"[object Object]"
三. 继承
1. 原型链继承
将父类的实例作为子类的原型
function People(name){
this.name = name;
}
People.prototype.sayHello = function(){
alert("你好我是" + this.name);
}
// var xiaoming = new People("小明");
// xiaoming.chifan();
function Student(name,xuehao){
this.name = name;
this.xuehao = xuehao;
}
//核心语句,继承的实现全靠这条语句了:
Student.prototype = new People('大明');
//Student.prototype.sayHello = function(){
// alert("你好我是小学生,我的学号是" + this.xuehao);
//}
Student.prototype.study = function(){
alert("好好学习,天天向上");
}
var xiaohong = new Student("小红",1001);
xiaohong.sayHello();
子类可以覆盖父类的一些方法,父类的方法不冲突,因为我们子类追加的方法,追加到了父类的实例上。
但是父类新增原型方法/原型属性,子类都能访问到,父类一变其它的都变了
2. 构造函数继承
使用父类的构造函数来增强子类实例,等于是复制父类的实例属性给子类(没用到原型)
function People(name){
this.name = name;
}
People.prototype.sayHello = function(){
alert("你好我是" + this.name);
}
function Student(name,xuehao){
// 核心语句
People.call(this,name);
// 这样就会在新parent对象上执行Person构造函数中定义的所有对象初始化代码
this.xuehao = xuehao;
}
Student.prototype.study = function(){
alert("好好学习,天天向上");
}
var xiaohong = new Student("小红",1001);
方法都在构造函数中定义, 只能继承父类的实例属性和方法,不能继承原型属性/方法,无法实现函数复用,每个子类都有父类实例函数的副本,影响性能
3. 组合继承
就是将原型链继承和构造函数继承组合在一起;继承两个优点
通过调用父类构造,继承父类的属性并保留传参的优点,
然后再通过将父类实例作为子类原型,实现函数复用
function People(name){
this.name = name;
}
People.prototype.sayHello = function(){
alert("你好我是" + this.name);
}
function Student(name,xuehao){
People.call(this,name);
this.xuehao = xuehao;
}
//核心语句,继承的实现全靠这条语句了:
Student.prototype = new People('大明');
Student.prototype.study = function(){
alert("好好学习,天天向上");
}
var xiaohong = new Student("小红",1001);
xiaohong.sayHello();
调用了两次父类构造函数,生成了两份实例(子类实例将子类原型上的那份屏蔽了)
4. 寄生组合继承
通过寄生方式,砍掉父类的实例属性,这样,在调用两次父类的构造的时候,就不会初始化两次实例方法/属性,避免的组合继承的缺点
function People(name){
this.name = name;
}
People.prototype.sayHello = function(){
alert("你好我是" + this.name);
}
function Student(name,xuehao){
People.call(this,name);
this.xuehao = xuehao;
}
//核心语句,继承的实现全靠这条语句了:
function Fn(){}
Fn.prototype = People.prototype
Student.prototype = new Fn();
Student.prototype.study = function(){
alert("好好学习,天天向上");
}
var xiaohong = new Student("小红",1001);
xiaohong.sayHello();
4. 圣杯模式
圣杯模式就是讲借用的构造函数封装一下
function inherit(People,Student){
function Fn(){}
Fn.prototype = People.prototype
Student.prototype = new Fn();
Student.prototype.constructor = Student;
Student.prototype.parent = People;
}
inherit(People,Student )