一、用new操作符调用函数
JS规定,使用new操作符调用函数会进行四步走:
/**
* new做了什么工作?
* 1.创建一个新的对象object
* 2.将新对象与构建函数通过原型链连接起来
* 3.将构建函数中的this绑定到新建的对象obj上
* 4.根据构建函数返回类型作判断,如果是原始值则被忽略 return this;如果是返回对象,需要正常处理 return 当前的值
*
* 流程如下:
* 1.创建一个空对象
* 2.将新对象的 __proto__ 指向为Person.prototype
* 3.将Person构造函数的this设置为新创建的对象,执行
* 4.构造函数Person没有return语句或者return的是简单数据类型 忽略,则将该新创建的对象返回 return this;如果return的是复杂的数据类型则返回的是当前return的值
*/
function MyNew(fun, ...args) {
// 1.创建一个新对象
const obj = {}
// 2.新对象原型指向构造函数原型对象
obj.__proto__ = fun.prototype
// 3.将构建函数的this指向新对象
let result = fun.apply(obj, args)
// 4.根据返回值判断
return result instanceof Object ? result : obj
}
// 测试
function MyNew(fun, ...args) {
const obj = {}
obj.__proto__ = fun.prototype
let result = fun.apply(obj, args)
return result instanceof Object ? result : obj
}
function Person0(name, age) {
this.name = name;
this.age = age;
}
Person0.prototype.sayHello = function () {
console.log("Hello,My name is " + this.name)
}
const person1 = MyNew(Person0, 'fatesgo', 20)
console.log(person1) // Person0 {name: "fatesgo", age: 18}
person1.sayHello() // 'Hello,My name is fatesgo'
二、什么是构造函数?
- 用new调用一个函数,这个函数就被称为构造函数,任何函数都可以是构造函数,只需要用new调用它
- 顾名思义,构造函数用来构造新对象,它内部的语句将为新对象添加若干属性和方法,完成对象的初始化
- 构造函数必须使用new关键字调用,否则不能正常工作,因此,开发者约定构造函数命名时首字母要大写
如果不用new调用构造函数
构造函数中的this不是函数本身
尝试为对象添加方法
为什么需要构造函数?因为我们一次创建一个对象,里面很多的属性和方法是大量相同的,代码就显得有些冗余,所以我们可以利用函数的方式,重复这些相同的代码,我们把这个函数称为“构造函数”,又因为这个函数不一样,里面封装的不是普通代码,而是“对象”,构造函数就是把我们对象里面一些相同的属性和方法抽象出来封装到函数里面
构造函数:是一种特殊的函数,主要用来初始化对象,即为对象成员变量赋初始值,它与new运算符一起使用,我们可以把对象中一些公共的属性和方法抽取出来,然后封装到这个函数里面。
构造函数与普通函数的区别:
1.普通函数不需要用new关键字调用而构造函数需要
2.普通函数可以用return,构造函数会返回一个新对象所以构造函数不需要return
3.this的指向问题,普通函数this指向的是window,构造函数this指向的是构造函数new出来的实例对象
4.函数命名建议首字母大写,与普通函数区分开(只是建议)
5.构造函数的属性和方法前面必须加this
三、类和实例的区别
3.1 在JavaScript中我们会像下面这样模拟一个类:
function Person(name,sex) {
this.name = name;
this.sex = sex;
}
Person.prototype.sayName = function() {
console.log(this.name)
}
var xiaoming = new Person('xiaoming','男');
xiaoming.sayName();
3.2 实例化
类定义后,就可以通过new
关键创建类实例:
var person = new Person('muzidigbig','男');
console.log(person.__proto__ === Person.prototype);//true
JavaScript中实例化不同与传统面向对象语言(如:Java、C++等),其实例化基于对象原型。
__proto__
是一个对象内部属性,继承自Object.prototype.__proto__
。当类被实例化时,对象的(类实例)的__proto__
属性会指向类的原型,即:类的prototype
属性。
这就是基于原型的类的实现方式,也就是原型链的实现方式。实例化后当调用对象的属性或方法时,会有如下过程:
- 查找对象是否有该属性或方法,如果则在则返回或调用
- 如果不存在,则通过
__proto__
属性,在原型链上查找有没有属性或方法
对象、函数都会有__proto__这个属性,对象并不具有prototype属性,只有函数才有prototype属性。
四、详解prototype与__proto__区别
对象、函数都会有_proto_这个属性,对象并不具有prototype属性,只有函数才有prototype属性。
__proto__
是每个对象都有的一个属性(原型链的路线),而prototype是函数(构造函数)才会有的属性!!!
使用Object.getPrototypeOf()
代替__proto__
!!!
1.每个函数function都有一个 prototype ,即显式原型
2.每个实例对象都有一个 __proto__ ,可称为隐式原型
3.对象的隐式原型的值为其对应构造函数的显示原型的值
4.总结:
4.1 函数的prototype属性:在定义函数时自动添加的,默认值是一个空Object对象
4.2 对象的__proto__属性:创建对象时自动添加的,默认值为构造函数的prototype属性值
4.3 程序员能直接操作显示原型,但不能直接操作隐式原型(ES6之前)
4.1 什么是prototype?
- 任何函数都有prototype属性,prototype表示“原型”的意思
- prototype属性值是一个对象,它默认拥有constructor属性指构造函数
- 普通函数来说的prototype属性没有任何用处,而构造函数的prototype属性非常有用
- 构造函数的prototype属性是它的实例的原型
几乎所有的函数(除了一些内建函数)都有一个名为prototype(原型)的属性(除null外),这个属性是一个指针,指向一个对象,而这个对象的用途是包含可以有特定类型的所有实例共享的属性和方法。prototype是通过调用构造函数而创建的那个对象实例的原型对象。
hasOwnProperty()
判断指定属性是否为自有属性;in操作符对原型属性和自有属性都返回true。
示例:自有属性&原型属性
var obj = {a: 1};
obj.hasOwnProperty("a"); // true
obj.hasOwnProperty("toString"); // false
"a" in obj; // true
"toString" in obj; // true
示例:鉴别原型属性
function hasPrototypeProperty(obj, name){
return name in obj && !obj.hasOwnProperty(name);
}
4.2 __proto__
对象(除null外)具有属性__proto__
,可称为隐式原型,一个对象的隐式原型指向构造该对象的构造函数的原型,这也保证了实例能够访问在构造函数原型中定义的属性和方法。
当使用 obj.__proto__ 时,可以理解成返回了 Object.getPrototypeOf(obj)。
function Person(){}
var Boo = {name: "Boo"};
Person.prototype = Boo;
var person = new Person();
console.log(person.__proto__ === Person.prototype); // true
console.log(person.__proto__ === Boo); // true
console.log(Object.getPrototypeOf(person) === person.__proto__); // true
console.log(Object.getPrototypeOf(person) === Person.prototype) // true
4.3 Object.getPrototypeOf()
一个对象实例通过内部属性[[Prototype]]
跟踪其原型对象。使用原型对象的好处是可以让所有对象实例共享它所包含的属性和方法。可以调用对象的Object.getPrototypeOf()
方法读取[[Prototype]]
属性的值,也可以使用isPrototypeOf()
方法检查某个对象是否是另一个对象的原型对象。大部分JavaScript引擎在所有对象上都支持一个名为__proto__
的属性,该属性可以直接读写[[Prototype]]
属性。
//Object.getPrototypeOf()方法读取实例的原型对象
console.log(Object.getPrototypeOf(student));
//isPrototypeOf()函数用于指示对象是否存在于另一个对象的原型链中。如果存在,返回true,否则返回false。
示例:原型对象
function Person(name) {
this.name = name;
}
// 原型对象
Person.prototype = {
constructor: Person,
sayName: function(){
console.log("my name is " + this.name);
}
}
var p1 = new Person("ligang");
var p2 = new Person("Camile");
p1.sayName(); // my name is ligang
p2.sayName(); // my name is Camile
4.4 constructor
每个原型都有一个constructor属性,指向该关联的构造函数。
function Person() {
}
console.log(Person===Person.prototype.constructor) //true
当获取 person.constructor 时,其实 person 中并没有 constructor 属性,当不能读取到constructor 属性时,会从 person 的原型也就是 Person.prototype 中读取,正好原型中有该属性,所以:
person.constructor === Person.prototype.constructor // true
person.__proto__.constructor === Person.prototype.constructor // true
五、实例与原型
当读取实例的属性时,如果找不到,就会查找与对象关联的原型中的属性,如果还查不到,就去找原型的原型,一直找到最顶层为止。
function Person() {
}
Person.prototype.name = 'Kevin';
var person = new Person();
person.name = 'Daisy';
console.log(person.name) // Daisy
delete person.name;
console.log(person.name) // Kevin
在这个例子中,我们给实例对象 person 添加了 name 属性,当我们打印 person.name 的时候,结果自然为 Daisy。
但是当我们删除了 person 的 name 属性时,读取 person.name,从 person 对象中找不到 name 属性就会从 person 的原型也就是 person.__proto__ ,也就是 Person.prototype中查找,幸运的是我们找到了 name 属性,结果为 Kevin。
但是万一还没有找到呢?原型的原型又是什么呢?
六、原型的原型
在前面,我们已经讲了原型也是一个对象,既然是对象,我们就可以用最原始的方式创建它,那就是:
var obj = new Object();
obj.name = 'Kevin'
console.log(obj.name) // Kevin
其实原型对象就是通过 Object 构造函数生成的,结合之前所讲,实例的 __proto__ 指向构造函数的 prototype ,所以我们再更新下关系图:
七、原型链
简单的回顾一下构造函数、原型和实例的关系:每个构造函数都有一个原型对象,原型对象都包含一个指向构造函数的指针,而实例都包含一个指向原型对象的内部指针。那么假如我们让原型对象等于另一个类型的实例,结果会怎样?显然,此时的原型对象将包含一个指向另一个原型的指针,相应地,另一个原型中也包含着一个指向另一个构造函数的指针。假如另一个原型又是另一个类型的实例,那么上述关系依然成立。如此层层递进,就构成了实例与原型的链条。这就是所谓的原型链的基本概念。——摘自《javascript高级程序设计》
其实简单来说,就是上述五-六的过程。
继上述五中所说,那 Object.prototype (最顶层)的原型呢?
console.log(Object.prototype.__proto__ === null) // true
引用阮一峰老师的 《undefined与null的区别》 就是:
null 表示“没有对象”,即该处不应该有值。
所以 Object.prototype.__proto__ 的值为 null 跟 Object.prototype 没有原型,其实表达了一个意思。
所以查找属性的时候查到 Object.prototype 就可以停止查找了。
1.原型链
访问一个对象的属性时,
先在自身属性中找,找到返回
如果没有,再沿着 __proto__ 这条链向上查找,找到返回
如果最终没找到,返回 undefined
别名:隐式原型链
作用:查找对象的属性(方法)
2.原型链——属性问题:
1.读取对象的属性值时:会自动到原型链中查找
2.设置对象的属性值时:不会查找原型链,如果当前对象中没有此属性,直接添加此属性并设置其值
3.方法一般定义在原型中,属性一般通过构造函数定义在对象本身上
什么是原型/原型链?
原型的本质就是一个对象。
当我们在创建一个构造函数之后,这个函数会默认带上一个prototype属性,而这个属性的值就指向这个函数的原型对象。
这个原型对象是用来为通过该构造函数创建的实例对象提供共享属性,即用来实现基于原型的继承和属性的共享
所以我们通过构造函数创建的实例对象都会从这个函数的原型对象上继承上面具有的属性
当读取实例的属性时,如果找不到,就会查找与对象关联的原型中的属性,如果还查不到,就去找原型的原型,一直找到最顶层为止(最顶层就是Object.prototype的原型,值为null)。
所以通过原型一层层相互关联的链状结构就称为原型链。
最后一张关系图也可以更新为:
八、继承
People和Student的关系
- People类拥有的属性和方法Student类都有,Student类还扩展了一些属性和方法
- Student是一种People,两类之间是“is a kind of”关系
- 这就是继承关系,Student类继承People类
- 继承描述了 两个类之间的关系,比如学生是一种人,所以人类和学生之间就构成继承的关系- People是“父类”(“超类”,“基类”);Student是“子类”(或者派生类)
8.2 JavaScript中如何实现继承
实现继承的关键在于:子类必须拥有父类的全部属性和方法,同时还应该能定义自己特有的属性和方法,使用特有的原型链特性来实现继承,是普遍的做法
通过原型链来实现继承
继承的实现很简单,只需要把子类的prototype设置为父类的一个对象即可。注意这里说的可是对象哦!
8.3 其他的继承方式:
方式一:原型链继承
/**
* 方式一:原型链继承
* 1.套路
* 1.定义父类型构造函数
* 2.给父类型的原型添加方法
* 3.定义子类型的构造函数
* 4.创建父类型的对象赋值给子类型的原型
* 5.将子类型原型的构造属性(.prototype.constructor)设置为子类型的构造函数
* 6.给子类型原型添加方法
* 7.创建子类型的对象:可以调用父类型的方法
* 2.关键
* 1.子类型的原型为父类型的一个实例对象
*
* 优点:写法方便简洁,容易理解。
* 缺点:对象实例共享所有属性和方法。无法向父类构造函数传参
*/
// 4步曲
// 第一步:父类型
function Supper() {
this.supProp = "Supper property";
}
Supper.prototype.showSupperProp = function () {
console.log(this.supProp);
}
// 第二步:子类型
function Sub() {
this.subProp = "Sub property";
}
// 第三步:子类型的原型为父类型的一个实例对象(为了看到父类型的方法)
Sub.prototype = new Supper();
// 第四步:让子类型的原型的 constructor 指向子类型(为了修正 constructor 属性)
Sub.prototype.constructor = Sub;
Sub.prototype.showSubProp = function () {
console.log(this.subProp);
}
var sub = new Sub();
sub.showSupperProp();
console.log(sub.constructor);
方式二:借用构造函数继承(假的)
/**
* 方式二:借用构造函数继承(假的)
* 1.套路
* 1.定义父类型构造函数
* 2.定义子类型构造函数
* 3.在子类型构造函数中调用父类型构造
* 2.关键
* 1.在子类型构造函数中使用 call() 调用父类型构造函数
*
* 优点:解决了原型链实现继承的不能传参的问题和父类的原型共享问题。
* 缺点:借用构造函数的缺点是方法都在构造函数中定义,因此无法实现函数复用。
* 在父类型的原型中定义的方法,对子类型而言也是不可见的,结果所有类型都只能使用构造函数。
*/
function Person(name,age) {
this.name = name;
this.age = age;
}
function Student(name,age,price) {
Person.call(this,name,age); // 相当于: this.Person(name,age) 为了得到属性 使用构造函数传递参数
this.price = price;
}
var s = new Student("Tom",22,14000);
console.log(s.name,s.age,s.price);
方式三:原型链+借用构造函数的组合继承
/**
* 方式三:原型链+借用构造函数的组合继承
* 1.利用原型链实现对父类型对象的方法继承
* 2.利用 call() 借用父类型构建函数初始化相同属性
*
* 优点:就是解决了原型链继承和借用构造函数继承造成的影响。
* 缺点:是无论在什么情况下,都会调用两次父类构造函数;
* 一次是在创建子类原型的时候,一次是在子类构造函数内部。
*/
function Person2(name,age) {
this.name = name;
this.age = age;
}
Person2.prototype.setName = function () {
this.name = name;
}
function Student2(name,age,price) {
Person2.call(this,name,age); // 相当于: this.Person(name,age)
this.price = price;
}
Student2.prototype = new Person(); // 为了看到父类型的方法
Student2.prototype.constructor = Student2; // 为了修正 constructor 属性
Student2.prototype.setPrice = function () {
this.price = price;
}
var s2 = new Student2("Tom",226,140005);
console.log(s2.name,s2.age,s2.price);
方式四:ES6、Class实现继承
/**
* 方式四:ES6、Class实现继承
* Class 通过 extends 关键字实现继承,其实质是创造出父类的this对象,然后用子类的构造函数修改this
* 子类的构造方法中必须调用 super() 方法,且只有在调用了 super() 之后才能使用this,
* 因为子类的this对象是继承父类的this对象,然后对其进行加工,而 super() 方法表示的是父类的构造函数,用来新建父类的this对象
*
* 优点:语法简单易懂,操作更方便。
* 缺点:并不是所有的浏览器都支持class关键字
*/
class Animal{
constructor(kind){
this.kind = kind
}
getKind(){
return this.kind
}
}
// 继承 Animal
class Cat extends Animal{
constructor(name){
// 子类的构造方法中必须先调用 super 方法
super('car');
this.name = name;
}
getCatInfo(){
console.log(this.name + ':' + super.getKind());
}
}
const cat1 = new Cat('buding');
cat1.getCatInfo(); // buding:car
Class
定义“类”的方法的时候,不需要加上function
这个关键字,直接把函数定义放进去了就可以了。另外,方法之间不需要逗号分隔,加了会报错。
1,类的数据类型就是函数,类本身就指向构造函数
2,类的所有方法都定义在类的prototype
属性上面
3
,
Object.assign
方法可以很方便地一次向类添加多个方法
4,类的内部所有定义的方法,都是不可枚举的
constructor方法
(1)constructor
方法是类的默认方法,通过new
命令生成对象实例时,自动调用该方法。一个类必须有constructor
方法,如果没有显式定义,一个空的constructor
方法会被默认添加。
(2) constructor
方法默认返回实例对象(即this
),完全可以指定返回另外一个对象
(3) 类必须使用new
调用,否则会报错。这是它跟普通构造函数的一个主要区别,后者不用new
也可以执行。
1.不存在变量提升:(class定义变量时)
2.Class里面super是干嘛的
super()执行父类的构造函数
super()返回的是子类的实例
3.为什么class中的方法可以使用箭头函数
class是ES6中的新语法,本质上并不是功能,只是对象原型的语法糖。让对象原型的写法更接近传统的面向对象语言(比如Java)
- class中的方法如果是普通函数方法,该方法会绑定在构造函数的原型上;
- 如果方法是箭头函数方法,该方法会绑定在构造函数上;
- 通过上述方式调用class中的方法,无论是箭头函数方法还是普通函数方法,方法中的this都指向实例对象。
九、面向对象到底是什么
- 面向对象的本质:定义不同的类,让类的实例工作
- 面向对象的优点:程序编写更清晰,代码结构更加严密,使代码更健壮易于维护
- 面向对象经常用到的场合,需要封装和复用性的场合(组件思维)
十、包装类
- Number()、String()和Boolean()分别是数字、字符串、布尔值的"包装类"
- 很对编程语言都有"包装类"的设计,包装类的目的就是为了让基本类型值可以从它们的构造函数的 propotype 上获得方法
- Number()、String()和Boolean()的实例都是Object类型,它们的PrimitiveValue属性存储它们的本身值
- new 出来的基本类型值可以正常参与运算
JavaScript没有传统面向对象语言的类继承机制,而是基于原型链继承实现的,其本质是使用函数模拟类的特征。我们可以通过prototype
将属性/方法写到原型对象上,通过new
操作符创建对象(实例化)时,实例对象会把类原型链上的属性关联到自身的__proto__
属性上;而子类继承父类时,是将子类的__proto__
属性指向父类的prototype
属性,并在子类__proto__
属性添加自己的方法和属性实现对父类的扩展。