进击JavaScript核心 --- (3)面向对象
var student = new Object(); student.name = 'Jim Green'; student.gender = 'Male'; student.age = 8; student.say = function() { console.log(`My name is ${this.name}, I'm ${this.age} years old`); } student.say(); // My name is Jim Green, I'm 8 years old
(2)、对象字面量
var student = { name: 'Jim Green', gender: 'Male', age: 8, say: function() { console.log(`My name is ${this.name}`) } } student.say(); // My name is Jim Green
2、属性类型
var person = { age: 8, gender: 'male' }; Object.defineProperty(person, "name", { enumerable: false, value: "Roger" }); for(var key in person) { console.log(`key --> ${key}, value --> ${person[key]}`); } // key --> age, value --> 8 // key --> gender, value --> male
// "use strict"; var person = {}; Object.defineProperty(person, "name", { configurable: false, writable: true, value: "Roger" }); console.log(person.name); // Roger delete person.name; console.log(person.name); // Roger (configurable属性为false时,返回undefined) // Uncaught TypeError: Cannot delete property 'name' of #<Object> (严格模式下报错)
// "use strict" var person = {}; Object.defineProperty(person, "name", { writable: false, value: "Roger" }) console.log(person.name); // Roger person.name = "Frank"; console.log(person.name); // Roger // console.log(person.name); // Uncaught TypeError: Cannot assign to read only property 'name' of object '#<Object>'
var person = {}; Object.defineProperty(person, "name", { configurable: true, value: "Roger" }); person.name = "Frank"; console.log(person.name); // Roger Object.defineProperty(person, "name", { writable: true, value: "Roger" }); person.name = "Kobe"; console.log(person.name); // Kobe
var person = {}; Object.defineProperty(person, "name", { configurable: false, value: "Roger" }); person.name = "Frank"; console.log(person.name); // Roger Object.defineProperty(person, "name", { // Uncaught TypeError: Cannot redefine property: name writable: true, value: "Roger" }); person.name = "Kobe"; console.log(person.name);
2-2、访问器属性
访问器属性不包含数据值,它们包含一对getter和setter函数(非必需)。共包含4个特性:
var person = { name: 'Kobe', _number: 8 } Object.defineProperty(person, 'number', { get: function() { return this._number; }, set: function(newValue) { if(newValue > 8) { this._number = 24; } } }) person.number = 9; console.log(person._number); // 24
var book = {}; Object.defineProperties(book, { _year: { writable: true, value: 8 }, year: { get: function() { return this._year; }, set: function(newValue) { if(newValue > 8) { alert('111') this._year = 24; } } } }) console.log(book); // {_year: 8} book.year = 9; console.log(book); // {_year: 24}
var book = {}; Object.defineProperties(book, { _year: { writable: true, value: 8 }, year: { get: function() { return this._year; }, set: function(newValue) { if(newValue > 8) { alert('111') this._year = 24; } } } }); var obj1 = Object.getOwnPropertyDescriptor(book, '_year'); var obj2 = Object.getOwnPropertyDescriptor(book, 'year'); console.log(obj1); // {value: 8, writable: true, enumerable: false, configurable: false} console.log(obj2); // {get: ƒ, set: ƒ, enumerable: false, configurable: false}
// 例如:Java中定义一个Student类 public class Student { private String name; // 姓名 private int age; // 年龄 public Student(String name, int age) { //构造器 super(); this.name = name; this.age = age; } // 属性的getter和setter方法 public String getName() { return name; } public void setName(String name) { this.name = name; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } // 自定义方法 public void say() { System.out.println("姓名:" + this.name + "年龄:" + this.age); } } // 通过Student类来创建实例对象 Student xiaoMing = new Student("XiaoMing", 12); Student xiaoHong = new Student("XiaoHong", 9); xiaoMing.say(); // 姓名:XiaoMing年龄:12 xiaoHong.say(); // 姓名:XiaoHong年龄:9
var xiaoMing = { name: "XiaoMing", age: 12 }; var XiaoHong = { name: "XiaoHong", age: 9 }
function createStudent(name, age) { var obj = new Object(); obj.name = name; obj.age = age; obj.say = function() { console.log(`My name is ${this.name}, I'm ${this.age} years old.`); }; return obj; } var xiaoMing = createStudent('XiaoMing', 12); var xiaoHong = createStudent('XiaoHong', 8); xiaoMing.say(); // My name is XiaoMing, I'm 12 years old. xiaoHong.say(); // My name is XiaoHong, I'm 8 years old.
function Student(name, age) { this.name = name; this.age = age; this.say = function() { console.log(`My name is ${this.name}, I'm ${this.age} years old.`); }; } var xiaoMing = new Student('XiaoMing', 12); var xiaoHong = new Student('XiaoHong', 8); xiaoMing.say(); // My name is XiaoMing, I'm 12 years old. xiaoHong.say(); // My name is XiaoHong, I'm 8 years old.
console.log(xiaoMing.constructor) // ƒ Student(name, age) { // this.name = name; // this.age = age; // this.say = function() { // console.log(`My name is ${this.name}, I'm ${this.age} years old.`); // }; // } console.log(xiaoMing.constructor == Student) // true console.log(xiaoMing.constructor == xiaoHong.constructor) // true console.log(xiaoMing instanceof Student) // true console.log(xiaoMing instanceof Object) // true
function Student(name, age) { this.name = name; this.age = age; this.say = function() { console.log(`My name is ${this.name}, I'm ${this.age} years old.`); }; } // 将Student当作构造函数(this指向实例对象) var xiaoMing = new Student('XiaoMing', 12); xiaoMing.say(); // My name is XiaoMing, I'm 12 years old. // 将Student当作普通函数(由于Student函数属于全局作用域,因此实际上是window.Student(),this指向window) Student('Bob', 9); say(); // My name is Bob, I'm 9 years old. // 在特定的作用域中调用函数(this指向obj) var obj = new Object(); Student.call(obj, 'Ryan', 30); console.log(obj); // {name: "Ryan", age: 30, say: ƒ} obj.say(); // My name is Ryan, I'm 30 years old.
// 上面的say方法等价于 function Student(name, age) { this.name = name; this.age = age; this.say = new Function(`My name is ${this.name}, I'm ${this.age} years old.`); }
function Student(name, age) { this.name = name; this.age = age; this.say = say; } function say() { console.log(`My name is ${this.name}, I'm ${this.age} years old.`); } var xiaoHong = new Student('XiaoHong', 8); xiaoHong.say(); // My name is XiaoHong, I'm 8 years old.
这样做的话同样存在问题,首先是封装性不太好,对象的某些属性必须依赖于全局的属性;其次,我们期望全局作用域内的函数say只能用于构造函数Student,这样就跟js的理念相冲突了
3-3、原型模式
function Student() { } Student.prototype.name = 'Bob'; Student.prototype.age = 12; Student.prototype.say = function() { console.log(`My name is ${this.name}, I'm ${this.age} years old.`); } var xiaoMing = new Student(); var xiaoHong = new Student(); xiaoMing.say(); // My name is Bob, I'm 12 years old. xiaoHong.say(); // My name is Bob, I'm 12 years old.
console.log(xiaoMing.__proto__); // {name: "Bob", age: 12, say: ƒ, constructor: ƒ} console.log(xiaoMing.__proto__ === Student.prototype); // true
console.log(Student.prototype.isPrototypeOf(xiaoMing)); // true console.log(Student.prototype.isPrototypeOf(xiaoHong)); // true // 说明实例对象 xiaoMing和xiaoHong都存在于Student.prototype的原型链上
console.log(Object.getPrototypeOf(xiaoHong)); // {name: "Bob", age: 12, say: ƒ, constructor: ƒ} console.log(Object.getPrototypeOf(xiaoHong) === Student.prototype); // true
xiaoMing.name = 'XiaoMing'; console.log(xiaoMing.name); // XiaoMing console.log(xiaoHong.name); // Bob
为了进一步对比,可以删除掉实例对象 xiaoMing 的属性name,然后再访问该属性
delete xiaoMing.name; console.log(xiaoMing.name); // Bob console.log(xiaoHong.name); // Bob
function Student() { } Student.prototype = { name: 'Bob', age: 12, course: ['Chinese', 'Math'] } var xiaoMing = new Student(); var xiaoHong = new Student(); xiaoMing.name = 'XiaoMing'; console.log(xiaoMing.name); // XiaoMing console.log(xiaoHong.name); // Bob xiaoMing.course.push('English'); console.log(xiaoMing.course); // ["Chinese", "Math", "English"] console.log(xiaoHong.course); // ["Chinese", "Math", "English"]
xiaoMing.name = 'XiaoMing'; console.log(xiaoMing.hasOwnProperty('name')); // true --- 来自实例 console.log(xiaoHong.hasOwnProperty('name')); // false --- 来自原型 // 实例对象xiaoMing有自己的name属性,xiaoHong则没有
xiaoMing.name = 'XiaoMing'; console.log('name' in xiaoMing); // true console.log(xiaoMing.hasOwnProperty('name')); // true console.log('name' in xiaoHong); // true console.log(xiaoHong.hasOwnProperty('name')); // false
xiaoMing.name = 'XiaoMing'; // 判断一个属性仅存在于对象的原型中 function checkPropertyInPrototype(Object, prop) { return (prop in Object) && !Object.hasOwnProperty(prop) } console.log(checkPropertyInPrototype(xiaoMing, 'name')); // false console.log(checkPropertyInPrototype(xiaoHong, 'name')); // true
function Student() { } Student.prototype.name = 'Bob'; Student.prototype.age = 12; Student.prototype.say = function() { console.log(`My name is ${this.name}, I'm ${this.age} years old.`); } var xiaoHong = new Student(); xiaoHong.gender = 'female'; for(var prop in xiaoHong) { console.log(prop +' --> '+ xiaoHong[prop]); } /* gender --> female name --> Bob age --> 12 say --> function() { console.log(`My name is ${this.name}, I'm ${this.age} years old.`); } */
function Person(name, age) { this.name = name; this.age = age; } Person.prototype = { say: function() { console.log(`My name is ${this.name}, I'm ${this.age} years old.`); } } var xiaoMing = new Person('XiaoMing', 12); var xiaoHong = new Person('xiaoHong', 9); xiaoMing.say(); // My name is XiaoMing, I'm 12 years old. xiaoHong.say(); // My name is xiaoHong, I'm 9 years old.
4、继承
JS中的继承主要是依靠原型链来实现的
4-1、原型链
简单回顾下构造函数、原型和实例对象的关系
(1)、每个构造函数都包含一个prototype属性指向原型对象
(2)、每个原型对象都包含一个constructor指针指向构造函数
(3)、每个实例都包含一个[[prototype]]指针指向构造函数的原型对象
原型链的基本思想就是利用原型,让一个引用类型继承另一个引用类型的属性和方法
具体说就是让原型对象等于另一个类型的实例,由于该实例包含指向其原型对象的指针,因此,此时的原型对象也包含了另一个原型对象的 指针。层层向上直到一个对象的原型对象是null。根据定义,null没有原型,并作为原型链中的最后一个环节
function Person() { } Person.prototype = { leg: 4, ear: 2 } function Student() { } // 让Student的原型对象等于Person的实例,因此Student的原型对象也包含了指向Person的原型对象的指针 Student.prototype = new Person(); // 给Student的原型对象添加方法 Student.prototype.say = function() { console.log(`I have ${this.ear} ears and ${this.leg} legs.`); } var student = new Student(); student.say(); // I have 2 ears and 4 legs. console.log(student.toString()); // [object Object] console.log(Object.getPrototypeOf(student) === Student.prototype); // true (student实例的原型对象就是Student.prototype) console.log(Person.prototype.isPrototypeOf(student)); // true (student实例存在于Person对象的原型链上) console.log(student instanceof Person); // true (student就是Person对象的实例) console.log(student.hello()); // Uncaught TypeError: student.hello is not a function
图中所示,红色的粗线条代表的就是原型链,绿色细线条代表是构造函数与其原型对象之间的关联。
执行 student.say(),student实例自己没有say方法,于是沿着原型链在它的原型对象是找到了,但是该原型对象中并没有ear和leg属性,由于该原型对象包含了指向Person原型对象的指针,因此,继续沿着原型链向上查找,在Person的原型对象上找到了ear和leg属性
执行 student.toString(),student实例没有提供该方法,于是沿着原型链逐级向上查找,由于所有引用类型都继承自Object,最终在Object的原型对象上找到了
执行 student.hello(),student实例没有提供该方法,沿着原型链逐级向上查找,都没有找到该方法,因此会报错
注意:
(1)、通过原型链实现继承时,不能使用对象字面量创建原型方法,否则会重写原型链
function Person() { } Person.prototype = { say: function() { console.log('hello world'); } } function Student() { } // 让Student的原型对象等于Person的实例,因此Student的原型对象也包含了指向Person的原型对象的指针 Student.prototype = new Person(); // 使用对象字面量给Student创建原型方法 Student.prototype = { class: 2, grade: 1 } var student = new Student(); student.say(); // Uncaught TypeError: student.say is not a function
此例中,先是把Person的实例赋值给Student的原型对象,构建出了一条图中红色粗线部分显示的原型链。然后,使用对象字面量的方法使得Student的原型对象指向了一个实例对象,切断了原来的原型链,重新构建出图中蓝色粗线部分的原型链,自然就找不到say方法了
(2)、给原型添加方法的代码一定要放在替换原型的语句之后。如子类型重写父类型中的某个方法,或在子类型中添加一个父类型中不存在的方法
function Person() { } Person.prototype = { say: function() { console.log('hello world'); } } function Student() { } // 让Student继承Person Student.prototype = new Person(); // 子类型重写父类型中的方法 Student.prototype.say = function() { console.log(`I'm Iron man`) } // 子类型中添加一个父类型中不存在的方法 Student.prototype.walk = function() { console.log('walk with legs') } var student = new Student(); student.say(); // I'm Iron man student.walk(); // walk with legs
首先确定了Student继承Person这一继承关系,Student原型对象可以读取到Person原型对象中的say方法,然后为Student原型对象添加的重写和新方法,会覆盖掉原来的say方法
function Person() { } Person.prototype = { say: function() { console.log('hello world'); } } function Student() { } // 子类型重写父类型中的方法 Student.prototype.say = function() { console.log(`I'm Iron man`) } // 子类型中添加一个父类型中不存在的方法 Student.prototype.walk = function() { console.log('walk with legs') } // 让Student继承Person Student.prototype = new Person(); var student = new Student(); console.log(Person.prototype.isPrototypeOf(student)); // true student.say(); // hello world student.walk(); // Uncaught TypeError: student.walk is not a function
与前面唯一的区别就是继承关系是在Student原型对象添加方法之后确定的,尽管student实例和Person的原型对象依然在同一条原型链上,但是会用Person原型对象中的属性和方法覆盖掉Student原型对象中的属性和方法,导致输出和前面的不一样
原型链弊端:
(1)、针对属性值是引用类型的情况,当某一个实例对象改变该共享属性时,其它实例也会随之改变
function Person() { this.course = ['chinese', 'math']; } function Student() { } Student.prototype = new Person(); var student1 = new Student(); console.log(student1.course); // ["chinese", "math"] student1.course.push('english'); var student2 = new Student(); console.log(student2.course); // ["chinese", "math", "english"]
(2)、创建子类型的实例时,不能向父类型的构造函数中传递参数。实际上就是一旦给父类型的构造函数传递参数,就会影响所有的实例对象
function Person(name, age) { this.name = name; this.age = age; } function Student() { } // 此处调用父类型的构造函数是需要传参的 Student.prototype = new Person('Bob', 12); var s1 = new Student(); var s2 = new Student(); console.log(s1.name); // Bob console.log(s2.name); // Bob
4-2、借用构造函数
用于解决原型中包含引用类型值所带来的问题
基本思想是在子类型构造函数中调用父类型的构造 函数,通过call()或apply()方法在(将来)新创建的对象上执行构造函数
function Person(name, age) { this.name = name; this.age = age; this.say = function() { console.log(`My name is ${this.name}, I'm ${this.age} years old`) } } function Student(name, age, grade) { Person.call(this, name, age); this.grade = grade; } function Teacher(name, age, height) { Person.apply(this, [name, age]); this.height = height; } var student = new Student("Jim", 9, 4); var teacher = new Teacher('Mr Lee', 33, 1.75); student.say(); // My name is Jim, I'm 9 years old teacher.say(); // My name is Mr Lee, I'm 33 years old
通过这种方式,子类型不仅可以继承父类型,还可以向父类型构造函数传递参数
如何解决原型中包含引用类型值带来的问题?
function Person() { this.course = ['chinese', 'math']; } function Student() { Person.call(this); } var s1 = new Student(); console.log(s1.course); // ["chinese", "math"] s1.course.push('english'); console.log(s1.course); // ["chinese", "math", "english"] var s2 = new Student(); console.log(s2.course); // ["chinese", "math"]
利用call方法将this绑定到了实例对象,所以即便修改了引用类型的值,也只是在实例对象的作用域范围内,不影响其他实例
弊端:方法都在构造函数中定义,如果在父类型的原型对象中定义方法,对子类型还是不可见的
function Person(name, age) { this.name = name; this.age = age; } Person.prototype.say = function() { console.log(`My name is ${this.name}`); } function Student(name, age) { Person.call(this, name, age); } var s = new Student("Jim", 2); s.say(); // Uncaught TypeError: s.say is not a function
4-3、组合继承
就是将原型链和借用构造函数两种方法组合在一起实现继承,这也是JS中最常用的继承模式
思路:使用原型链实现对原型属性和方法的继承,通过借用构造函数实现对实例属性的继承
function Person(name) { this.name = name; this.course = ['chinese', 'math']; } Person.prototype.say = function() { console.log(`My name is ${this.name}`); } function Student(name, age) { Person.call(this, name); // 继承实例属性 this.age = age; } // 继承原型属性和方法 Student.prototype = new Person(); // 添加子类方法 Student.prototype.sayAge = function() { console.log(`I'm ${this.age} years old`); } // 重写父类方法 Student.prototype.say = function() { console.log(`My name is ${this.name}, I'm ${this.age} years old`); } var s1 = new Student('Bob', 8); // 修改引用类型值的属性 s1.course.push('english'); s1.sayAge(); // I'm 8 years old s1.say(); // My name is Bob, I'm 8 years old console.log(s1.course); // ["chinese", "math", "english"] var s2 = new Student('Jim', 11); console.log(s2.course); // ["chinese", "math"]