一、面向对象介绍
1.1、面向对象和面向过程区分
- pop面向过程:
- 按流程来按步骤一步步实现,重心放在完成的每个过程上;
- 性能比面向对象高,适合跟硬件联系很紧密的东西,但是不易维护、不易复用、不易扩展。
- oop面向对象:
- 把事务分解成一个一个对象,每个对象都是一个功能中心,明确分工。以功能来划分问题,而不是步骤;
以封装的思想,重心放在解决问题需要的对象身上,然后通过对对象的操作来完成相应的功能。 - 性能比面向过程低,更耗资源,但是易维护、易复用、易扩展。
- 把事务分解成一个一个对象,每个对象都是一个功能中心,明确分工。以功能来划分问题,而不是步骤;
1.2、面向对象三大特性(了解)
1.2.1、封装
定义:使用对象封装一些变量和函数
作用:复用和信息隐藏
详解:封装,也就是把客观事物封装成抽象的类,并且类可以把自己的数据和方法只让可信的类或者对象操作,对不可信的进行信息隐藏。
1.2.2、继承
定义:一个类获取另外一个类属性和方法的一种方式
作用:代码复用
详解:继承,就是可以使用已创建好的类的所有功能,并在无需重新编写原来的类的情况下对这些功能进行扩展。
- 通过继承创建的新类称为“子类”或“派生类”。
- 被继承的类称为“基类”、“父类”或“超类”。
- 继承的过程,就是从一般到特殊的过程。
1.2.3、多态
定义:同一个操作,作用于不用的对象,会有不同的行为
作用:具有可拓展性
详解:多态首先是建立在继承的基础上的,先有继承才能有多态。多态是指不同的子类在继承父类后分别都重写覆盖了父类的方法,即父类同一个方法,在继承的子类中表现出不同的形式。js天生就具备多态的特性(弱类型语言)
二、创建对象
- 简单工厂创建对象
- 自定义构造函数创建对象
2.1、字面量方式创建对象
直接使用字面量方式创建对象比较方便,以键值对{ key:value }的格式来定义数据
// 字面量创建对象let obj = { // 对象封装的好处: 数据安全,全局容易被修改,对象内部不容易被修改(更多的是一种解决方案,一种编程思想)name:'张三',age:18,ff:function(){console.log('方法');}}// 对象成员的删除delete obj.name; // 不建议 有可能这个对象你同事创建的obj.name = null; // 建议 初始化为null
优点:方便直观,可以直接访问里面的属性方法;
缺点:只能创建单个对象,不能批量创建
2.2、内置构造函数创建对象
使用new关键字+内置的构造函数创建对象
let obj = new Object();obj.name = '张三';obj.age = '18';obj.ff = function () {console.log('方法');}obj.ff();
缺点:只能创建单个对象,不能批量创建
2.3、简单工厂函数创建对象
通过函数封装的思想将重复代码抽取出来封装进函数内
function hs(name, age) {// 1. 内部创建空对象let obj = new Object();// 2. 给空对象添加属性obj.name = name;obj.age = age;// 3. 返回内部创建的对象return obj;}// 可以不用newvar hs1 = hs('张三', 18);var hs2 = hs('李四', 22);var dog = hs('狗', 5);console.log(hs1,hs2);console.log(dog);
优点:批量创建对象
缺点:无法判断对象的类型(人狗不分),console.log打印出来都是Object类型
2.4、自定义构造函数创建对象
// 面试题: 说出构造函数是如何创建对象的??? abcd// 也叫作: new的过程,实例化的过程function Person(name,age){// a. 内部会默认创建空对象 ---------- var obj = new Object()// b. 给空对象添加属性并赋构造函数的原型 --------- obj.__proto__ = Person.prototype// c. 将创建好的空对象赋值给this ---- this = obj// d. 给this添加成员 ------------- this.xxx = xxxthis.name = name;this.age = age;// e. 自动返回this --------------- return this}// 必须new创建实例对象 步骤abcde才生效var p1 = new Person('张三', 18);var p2 = new Person('李四', 22);
优点:能判断对象的类型,console.log打印出来是Person类型
2.5、工厂函数和自定义构造函数的区别
- 构造函数名的首字母要求大写
- 在函数中,不需要手动创建对象进行数据封装,会自动创建对象并封装数据
- 在函数最后,不需要手动返回创建好的对象,会自动返回
- 构造函数一样可以直接调用,此时内部的this执行window,这种方式不太安全,有可能会在函数内部修改当前的全局变量,不建议使用,而且这样做也不能创建对象,必须要搭配new关键字一起使用才能创建对象
- 面试题
var Fun = function(){this.name="pp";// return "j"; // 构造函数返回基本类型 不改变实例的指向 返回无效return {name:"jj"};// 构造函数返回引用地址 实例=返回的对象}var p = new Fun();console.log(p.name); // jj// return "j";返回的是pp
三、构造器属性
3.1、类和对象区分
- 构造器(类):
- 泛指一类事物
- 把多个对象相同的部分抽象出来,成为一个类,就是一个函数,构造函数;
js中没有严格的类的概念 一定要说有———-构造函数(模拟面向对象语言)
- 对象:
- 特质某一个具体事物
- 使用构造函数new创造出来的对象
3.2、constructor属性和instanceof关键字
- 需求:定义Person和Dog类,并各自创建出对象,判断对象是否是Person或Dog类型
- constructor属性
constructor 制造者的意思:可以获取到创建对象使用的构造器函数(类)。 ```javascript function Person(name) { this.name = name; } function Dog(name) { this.name = name; } let p = new Person(‘张三’); let d = new Dog(‘狗’);
// 实例化对象的原型对象prototype的constructor原型 if(p.proto.constructor === Person){ console.log(“p是Person对象”); } if(d.proto.constructor === Dog){ console.log(“d是Dog对象”); }
2. **instanceof关键字**<br />instanceof 实例的意思:判断a(实例对象)的**类型**是不是b(构造函数类) 返回布尔值```javascriptfunction Person(name) {this.name = name;}function Dog(name) {this.name = name;}let p = new Person('张三');let d = new Dog('狗');console.log(p instanceof Person);//trueconsole.log(d instanceof Person);//false
3.3、构造函数中的this指向
- 构造函数中的this指向实例对象
var _this = null;function Person(name, age) {console.log(this); // 构造函数中的this指向创建出来的新对象(也就是实例对象)_this = this;this.name = name;this.age = age;this.ff = function () {console.log(this);}}let p = new Person('张三', 18);let p2 = new Person('李四', 22);p.ff(); // 谁调用this指向谁console.log(_this);// 指向p2,p被覆盖了(第二次创建p2,_this又执行了一次)
四、原型对象
4.1、自定义构造函数存在的问题
问题:对象1和对象2的ff方法是同一个内存地址吗?
function Star(uname) {this.uname = uname;this.ff = function () {console.log('方法');}}let dx1 = new Star('张三');// console.log(Star.newUname = '新张三'); // 静态成员就是在构造函数本身上添加的成员// console.log(dx1.uname); // 实例成员就是在构造函数内部this添加的成员,只能通过对象的实例化来访问// console.log(Star.uname); // 不能通过构造函数来访问实例成员// console.log(dx1.newUname); // 不能通过实例化对象来访问静态成员let dx2 = new Star('李四');console.log(dx1.ff===dx2.ff); // 输出false (new两个实例化对象里面共有的方法都会重新开辟内存空间,代码一样,浪费)
上面创建的p对象的内存结构图:
结论:从内存资源分配考虑,我们无需为每个对象创建并分配一份新的函数对象(完全相同),这种函数大家最好共享同一份。
解决办法:那么我们可以把函数单独拿出来,定义全局,函数名做方法对象:
function ff(){console.log('方法');}function Star(name) {this.name = name;this.say = say; // 每一次都赋值 全局函数ff 的内存地址}let dx1 = new Star('张三');let dx2 = new Star('李四');console.log(dx1.ff===dx2.ff); // 输出true 减小的内存的浪费
结论:解决方法共享的目的是达到了,但是又产生了新的问题,我们面向对象编程的目的是为了减少全局变量,而这种写法又增加了全局变量,与我们编程思想产生了冲突。
完美的解决办法:这个时候,我们需要在构造函数上想办法,在构造函数身上开辟一个区域,来存放我们的公共方法——原型对象
4.2、面向对象中的核心概念
- 构造函数:Star,和new关键字一起创建对象
- 静态成员:直接添加在构造函数上的属性和方法,只能使用构造函数才能访问
- 原型对象:Star.prototype
- 实例化:由构造器创建实例对象new的过程称之为实例化
- 实例对象:由构造器创建出来的对象称之为实例对象
- 实例成员:实例对象上的属性和方法,name,age,只能当前实例对象才能访问
- 原型成员:原型对象上的属性和方法,say(),使用该原型对象对应构造器创建出来的所有实例对象都能访问
function Star(uname) { // 1.构造函数 ---- 构造器(类) Starthis.uname = uname; // 5.实例成员 uname}// 2.构造函数的原型对象prototype (所有实例对象共享的,公共的方法放原型对象里面共享)Star.prototype.ff = function () { // 6.原型成员 ffconsole.log('prototype原型对象里面的方法');}let dx1 = new Star('张三'); // 3.实例对象: 构造函数创建出来的新对象 dx1let dx2 = new Star('李四'); // 4. 实例化: new的过程 构造函数创建实例对象的过程// 先查看实例对象dx1身上有没有ff这个方法,没有就通过__proto__ prototype查找ff这个方法console.log(dx1.ff === dx2.ff); // 输出true(共有所以相等)// 7.静态成员 ---- 给构造函数身上添加的成员(也可以是方法)Star.staticName = '静态成员';// console.log(dx1.staticName);// 实例对象不能使用静态成员console.log(Star.staticName);// 静态成员只能给构造函数使用
4.3、获取原型对象的方法
4.3.1、 proto 属性
定义:在每个实例对象上都有一个 proto 的属性,也是用来获取该对象的原型对象(该属性是在ES6之后才纳入规范,在这之前,只有部分浏览器实现)
// __proto__是实例对象的属性:意义就在于为对象的查找机制提供一个反向,或者说是一条路线,但是他是一个非标准属性,// 因此实际开发中,不可以使用这个属性,它只是内部指向原型对象prototypefunction Star(uname) {this.uname = uname;}let dx1 = new Star('张三');console.log(dx1.__proto__); // 实例化对象身上系统自带__proto__指向我们的原型对象(现在的控制台打印出来是[[prototype]])// prototype(构造函数的原型对象)和__proto__(实例对象的原型)是等价的console.log(Star.prototype === dx1.__proto__); // 输出true
4.3.2、getPrototypeOf方法
除了prototype和 proto能获取原型对象,getPrototypeOf方法也能
语法:Object.getPrototypeOf(实例对象) 获取指定实例对象的原型对象
// Object.getPrototypeOf()方法 是 Object内置构造函数的静态成员// 作用: 获取某个实例对象的原型对象console.log(Object.getPrototypeOf(dx1)===Star.prototype);console.log(Object.getPrototypeOf(dx1)===dx1.__proto__);
五、原型对象的设置和访问(掌握)
5.1、 proto指向
function Person(name, age) {this.name = name;this.age = age;}// 不会报错 因为new实例化的时候 p.__proto__ = Person.prototype(__proto__指向了Person.prototype)//(prototype本来就默认存在)var p = new Person('张三', 18);Person.prototype.ff = function () {console.log('prototype原型对象里面的方法');}p.ff(); // 先实例对象后给prototype添加方法并调用不会报错
5.2、设置原型遇到的问题
- 使用
构造器.prototype.方法名 = 方法函数动态添加方法 - 如果需要在原型对象上添加多个成员的时候用
构造器.prototype = {方法名:方法}
相对于之前的方式,这种方式能够更统一、更方便的在Person原型对象上添加成员。
// 1.先修改原型,再实例化并调用字面量对象{}里面的方法(不会报错)Person.prototype = {ff: function () {console.log('方法一');},ff2: function () {console.log('方法二');}}let p = new Person('张三',18);p.ff();
// 2.先实例化,再改原型并调用字面量对象{}里面的方法(会报错)let p = new Person('张三', 18);Person.prototype = {ff: function () {console.log('方法一');},ff2: function () {console.log('方法二');}}p.ff(); // Uncaught TypeError: p.ff is not a function
报错原因:实例化时的实例对象 proto已经指向了Person.prototype
后面再改实例对象为新字面量{}时实例对象不认,所以字面量{}里面的方法自然也不能调用
5.3、prototype = {} constructor指向问题修改
- 字面量对象的原型对象是Object.prototype
let obj = {name: '张三',}console.log(obj.__proto__); // 原型对象console.log(obj.__proto__.constructor === Object); // true 原型对象的构造函数是Object// 结论: 1.字面量对象的__proto__指向内置构造函数Object的原型对象// 2. 字面量对象是由内置构造函数Object实例化出来的
- 解决方法,手动的利用constructor指回原来的构造函数
function Person(name) {this.name = name;}// = 赋值操作了把实例对象的__proto__改向了赋值操作的字面量对象{}// prototype里面的constructor属性给覆盖掉了,原来的Star.prototype将不再被引用(会被内存销毁)Person.prototype = {// 如果我们修改了原来的原型对象,给原型对象赋值的是一个对象,则必须手动的利用constructor指回原来的构造函数constructor: Person,ff: function () {console.log('方法一');},ff2: function () {console.log('方法二');}}let p = new Person('张三');console.log(p.__proto__.constructor === Person); // true 原型对象的构造函数是Person
六、原型相关属性学习(掌握)
- 需求: 判断成员的归属 (静态成员,实例成员,原型成员)
function Person(name, age) {this.name = name;this.age = age;}Person.prototype.ff = function () {console.log('方法');}var p = new Person('张三', 18);
6.1、in关键字
- 语法:
''属性名'' in 实例对象———— 判断属性是不是 对象的实例成员或原型成员- 无论判断的成员是属于当前实例对象还是属于其原型对象的,都返回true,如果都不存在,则返回false。
for (var key in p) {console.log(key); // name age}console.log("name" in p); // trueconsole.log("ff" in p); // true
6.2、hasOwnProperty方法
- 语法:
实例对象.hasOwnProperty(''属性名'')———- 判断属性是不是 只属于对象的实例成员- 只判断当前实例对象中是否存在实例的属性,存在返回true,反之返回false。
- Object的原型成员,所有实例对象都能访问的方法
console.log(p.hasOwnProperty('name')); // trueconsole.log(p.hasOwnProperty('ff')); // false// 需求: 判断一个属性只属于原型成员console.log('name' in p && !p.hasOwnProperty('name')); // falseconsole.log('ff' in p && !p.hasOwnProperty("ff"));// true
6.3、isPrototypeOf方法
- 语法:
某个原型对象.isPrototypeOf(实例对象)——— 判断原型对象在不在实例对象的原型链上- A.isPrototypeOf(B) 判断的是A对象是否存在于B对象的原型链之中
console.log(Person.prototype.isPrototypeOf(p)); // trueconsole.log(Object.prototype.isPrototypeOf(p)); // trueconsole.log(Array.prototype.isPrototypeOf(p)); // false
6.4、instanceof关键字
- 语法:
实例对象 instanceof 构造函数- 字面意思理解为判断当前对象是否是指定的类型
- 更深层次理解,指定类型是否在当前实例对象的原型链上,如果是返回true,反之返回false。
console.log(p instanceof Person);// trueconsole.log(p instanceof Object);// trueconsole.log(p instanceof Array);// false
七、继承
ES5是没有继承语法的,只能通过方法模拟继承语法,所以有多种方式达到继承效果。
继承目的:通过相应的代码将父类中的成员复制到子类对象中
7.1、混入式继承(了解)
实现原理:将父类成员拷贝到子对象中(浅拷贝)。
实现方法:for…in…循环遍历父类,子类[key]=父类[key]
弊端:共享数据安全问题,修改子类,会影响父类,引用数据类型浅拷贝,会修改引用地址
//混入式继承(拷贝继承)//obj2继承到obj1中的成员,可以直接将obj1中的成员拷贝到obj2中即可var obj1 = {name: '张三',age: 18,dx:{dxName:'李四'}};var obj2 = {};// 将obj1中的成员拷贝到obj2中for (var key in obj1) {obj2[key] = obj1[key];}//修改obj1对象中的dx属性的属性值,会造成一改全改obj1.dx.dxName = '王五';console.log(obj1); // {name: '张三', age: 18, dx: {dxName: '王五'}}console.log(obj2); // {name: '张三', age: 18, dx: {dxName: '王五'}}
当我们需要修改其中某一个对象中的引用类型属性时,会造成其他相关的对象也被修改,原因在于大家引用的是同一个内存区域中的数据。
7.2、原型式继承(了解)
实现原理:将父类中的原型成员添加到子类的原型链中。
实现方式:子类.prototype = 父类.prototype
弊端:数据安全,子类会修改父类原型对象 造成原型链结构混乱
// 父类function Person(name) {this.name = name;}Person.prototype.ff = function () {console.log('prototype原型对象里面的方法');}// 子类function Son(name, age) {this.name = name;this.age = age;}// 子类的原型对象指向父类的原型对象(地址)Son.prototype = Person.prototype;// 在Son新原型中添加新方法Son.prototype.ff2=function(){console.log('新方法');}var son = new Son('张三', 18);son.ff(); son.ff(); // 成功调用父类的ff方法console.log(Person.prototype); // Person原型对象多了个ff2
7.3、原型链继承(掌握)
实现原理:将子类的原型对象指向父类构造函数。
实现方法:子类.prototype = new 父类()
存在问题:存在数据共享问题,无法给父类构造函数传递参数
弊端:只能继承原型成员不能继承实例成员
// 父类function Person(name, age) {this.name = name;this.age = age;}Person.prototype.ff = function () {console.log('prototype原型对象里面的方法');}// 子类function Son(name, age) {this.name = name;this.age = age;}// 1. 实现原型链继承 子类原型指向父类实例(新地址Person)Son.prototype = new Person(); // 不需要传参// 2. 注意点: 记得将constructor属性指向子类Son.prototype.constructor = Son;// 3. 子类需要方法,记得加上Son.prototype.ff2 = function () {console.log('新方法');}// 创建子类的实例var son = new Son('张三', 18);son.ff(); // 成功调用父类的ff方法console.log(son.__proto__.constructor); // 指向了子类Sonconsole.log(Person.prototype); // Person原型对象不会多了个ff2
7.4、借用构造函数继承(掌握)
7.4.1、call方法和apply方法的基本使用(十分重要)
- call语法:
方法.call(对象,参数1,参数2) - apply语法:
方法.apply(对象,[参数1,参数2.....])
call和apply的相同点: 将方法借给另一个对象使用并自动调用,会将方法中的this指向借给的对象
call和apply的不同点: 传参方式不同
let obj1 = {name: '张三',age: 20,add(a, b) {console.log(this.name);console.log(a + b);return a + b;}}let obj2 = {name: "李四",age: 50,}// 将obj1的add方法借给obj2并马上调用obj1.add.call(obj2, 100, 200); // 300 注意此时add方法里面this.name打印的是李四!obj1.add.apply(obj2, [100, 200]);
拓展
// 拓展: 获取数组中的最大值var arr = [1, 2, 4, 6, 8, 3];// max 借给谁不重要, 只要传参方式 .max刚刚好需要传数组参数用applyconsole.log(Math.max.apply(null,arr));
7.4.2、借用构造函数继承说明
实现原理:在子构造函数中调用父构造函数,达到继承并向父构造函数传参的目的。
实现方法:
- 将父对象的构造函数设置为子对象的成员
- 调用这个方法,类似于将父构造函数中的代码复制到子构造函数中来执行
- 用完之后删掉
弊端:只能继承实例成员不能继承原型成员(和原型链继承刚刚好相反)
如果父子构造函数存在相同的成员,那么子构造函数会覆盖父构造函数中的成员
// 需求: 子类继承父类的实例成员// 好处,子类里面的name和age父里面有但他也有代码冗余,这样写只要拿到父里面的name跟age就行function Person(name, age) {this.name = name;this.age = age;}function Son(name, age, score) {// Person(name, age);// 函数之间调用, 造成this指向window的问题// 重点: 改变this指向// 思路:// 1. 先将父类构造函数赋值给子类的实例对象this.fn = Person;// 2. 子类实例去调用这个方法------父类内部的this指向子类的实例this.fn(name, age);this.score = score;// 3. 用完之后,删掉delete this.fn;}var son = new Son('张三', 18, 50);console.log(son); // {name: '张三', age: 18, score: 50}
高级实现方法:凡是要借用方法,首先想到使用call或apply
function Student(name,age,score) {// 把父构造函数借给自己来调用,解决this指向window的问题Person.call(this,name,age);Person.apply(this,[name,age]);this.score = score;}
7.5、组合继承(必须掌握)
实现原理:基原型链继承+借用构造函数继承。
弊端: 子类的原型对象中有无效数据
function Person(name, age) {this.name = name;this.age = age;}Person.prototype.eat = function () {console.log(this.name);}function Son(name, age, score) {// 借用构造函数继承Person.apply(this, [name, age]);this.score = score;}// 原型链继承Son.prototype = new Person();Son.prototype.constructor = Son;Son.prototype.zff = function () {console.log(this.score);}var son = new Son('张三', 18, 50);son.eat(); // 张三son.zff(); // 50// 子类的原型对象中有无效数据console.log(son.__proto__); // {name: undefined, age: undefined, constructor: ƒ, zff: ƒ}
7.6、寄生组合继承(拓展)
实现原理:优化原型链继承(优化也叫寄生式继承)+借用构造函数继承。组合起来就叫寄生组合继承
组合继承的基础上,改造原本的原型链继承。子类和父类中间创建一个空类,过滤掉无用的父类实例属性。
function Person(name, age) {this.name = name;this.age = age;}Person.prototype.eat = function () {console.log(this.name);}function Son(name, age, score) {// 借用构造函数继承Person.apply(this, [name, age]);this.score = score;}// // 原型链继承// Son.prototype = new Person();// Son.prototype.constructor = Son;// 寄生式继承--------对原型链继承的优化// 思路: 创造一个中间类,过滤无效数据(function () {// 立即执行函数:目的是只执行一次 Super 用完之后就会删除// 函数中的变量在函数执行完毕之后 就会销毁// 函数局部变量的生命周期-------从创建到销毁的过程var Super = function () { };Super.prototype = Person.prototype;Son.prototype = new Super();Son.prototype.constructor = Son;})()Son.prototype.zff = function () {console.log(this.score);}var son = new Son('张三', 18, 50);son.eat(); // 张三son.zff(); // 50// 子类的原型对象中有没有无效数据console.log(son.__proto__); // {constructor: ƒ, zff: ƒ} 注意:无效数据被筛出了!
总结: ES5实现继承的方式不止一种。这是因为ES5 中的继承机制并不是明确规定的,而是通过模仿实现的。这意味着所有的继承细节并非完全由解释程序处理。作为开发者,你有权决定最适用的继承方式。
八、绘制完整的原型链结构图(掌握)
完整的结构图如下:
图1:
图2:
总结:
- Function函数对象由自身的构造函数创建
- Function的原型对象是一个匿名空函数,绑定了函数中的通用方法
- 空函数对象的类型是Object
- 所有的函数对象都是Function类型,由Function构造函数创建
- 所有的对象都是 Object 类型的
- 所有对象的 proto 最终都能找到 Object.prototype
// 1.问题: 函数也是对象,原型链结构怎样的??? 函数的__proto__指向???----- Function.prototypeconsole.log(Person.__proto__);console.log(Student.__proto__);console.log(Object.__proto__);console.log(Object.__proto__ === Student.__proto__);// 2.问题: 所有函数的构造函数是???----- 内置构造函数Functionconsole.log(Object.__proto__.constructor);console.log(Student.__proto__.constructor);console.log(Person.__proto__.constructor);// 3.问题: Function.prototype 看做是一个实例对象 她是类型是什么???----- Objectconsole.log(Function.prototype.__proto__.constructor);// 4. 问题: Function也可以看做是一个实例对象,对应的类型和原型对象是????// Function 的 类型 是 Functionconsole.log(Function.__proto__ === Function.prototype);
九、基本包装类型的使用(理解)
9.1、基本包装类型的创建
定义:方便对string,number,boolean三种基本类型数据操作,ECMAScript提供了三个特殊引用类型——基本包装类型(String,Number,Boolean)
创建方式:
// 方式一:var str1 = new String("string1");var num1 = new Number(123);var bool1 = new Boolean(true);console.log(str1);console.log(num1);console.log(bool1);// 方式二var str2 = new Object("string2");var num2 = new Object(456);var bool2 = new Object(false);console.log(str2);console.log(num2);console.log(bool2);
9.2、基本包装类型使用的注意点
先看下面的代码,观察存在的问题:
var str = '一个字符串';console.log(str.length);// 5console.log(str.substr(0, 2));// 一个
上面代码看似很简单,但是存在一个很基本的疑问点:
str是基本类型,不存在属性和方法一说,但是下面两行代码中却是在访问属性和方法,而且代码还能正确的执行,这是如何实现的呢?
其实,为了让我们实现这种直观的操作,后台已经自动完成了以下操作:
- 创建 String 类型的一个实例;
- 在实例上调用指定的方法;
- 销毁这个实例;
/* 底层实现:1. 创建了一个String的实例 var obj = new String(str);2. 包装类型去使用属性方法(原型成员的共享) obj.length obj.substr(0,2)3. 老渣男行为: 用完删掉 obj = null;*/// 所以基本数据类型str才能使用length属性和substr方法console.log(str.length);console.log(str.substr(0,2));
经过此番处理,基本的字符串值就变得跟对象一样了。而且,上面这三个步骤也分别适用于 Boolean和 Number 类型对应的布尔值和数字值
十、私有成员和特权方法(了解)
- 成员:对象的属性和方法的统称
实例成员:实例对象的属性和方法
原型成员:构造函数对应的原型对象的属性和方法
静态成员:构造函数自己的属性和方法
私有成员:在构造函数中声明的变量和函数,因为我们只能在函数内部访问,所以是私有的
特权方法:在函数内部使用了私有成员的实例方法被称为是特权方法(闭包)
// 构造函数 Personfunction Person(name, age) {// 构造函数内部的局部变量和函数--------私有成员 syName syFfvar syName = '私有成员';function syFf() {return '私有方法';}// 实例成员 name age slFfthis.name = name;this.age = age;// 使用了私有成员的实例方法------特权方法(函数的私有成员拿到外部使用)this.slFf = function () {console.log(syName, syFf());}}// 原型成员 yxFfPerson.prototype.yxFf = function () {console.log('原型方法');}// 静态成员 jtNamePerson.jtName = '静态成员';// 实例对象 pvar p = new Person('张三', 18);// p.syFf(); 报错 需要特权方法带他出来p.slFf(); // 私有成员 私有方法p.yxFf(); // 原型方法console.log(p.name, p.age); // 张三 18console.log(Person.jtName); // 静态成员
十一、Object成员(了解)
11.1、原型成员
- constructor:获取当前对象的构造函数
- hasOwnProperty:判断当前实例对象中是否存在指定的属性
- isPrototypeOf:判断当前对象是否在指定对象的原型链中
- propertyIsEnumerable:实例成员是否可以枚举(循环遍历)返回布尔值
function Person(name, age) {this.name = name;this.age = age;}Person.prototype.yxFf = function () {console.log('原型方法');}var p = new Person("张三", 20);// 1. propertyIsEnumerable 判断枚举性(可否遍历循环)------只能判断对象实例成员// 重点: 理解枚举的概念--- js自带的属性方法一般都不可枚举(打印出来颜色比较浅),可枚举(打印出来颜色比较深)for (var k in p) {// constructor 属性没被遍历???? 因为不可枚举console.log(k); // name age yxFf}console.log("---------把Person.prototype看做是Object构造函数创建的一个实例对象----------");console.log(Person.prototype.propertyIsEnumerable("yxFf")); // true 可枚举console.log(Person.prototype.propertyIsEnumerable("constructor")); // false 不可枚举// 不能判断原型成员console.log("---------p本来就是实例成员 使用这个方法没意义----------");console.log(p.propertyIsEnumerable("yxFf"));console.log(p.propertyIsEnumerable("constructor"));
- toString:返回数据特定的格式的字符串 [object 构造函数]。
- 几乎所有的构造函数都有原型成员toString,所以字符串,数字,布尔,数组等类型使用toString是使用自己构造函数的toString原型方法
- Object.prototype.toString.call(…);才能真正使用Object的toString原型方法
- 作用:可以获取所有数据的真实类型 ```javascript // 2.toString———-转字符串用 console.log(‘字符啊啊’.toString()); // 字符啊啊 console.log((10).toString()); // 10 console.log(true.toString()); // true console.log((new Date()).toString()); // Sun Aug 28 2022 16:48:57 GMT+0800 (中国标准时间) console.log([1, 2, 3].toString()); // 1,2,3 // 上述五个的数据类型调用的都不是 Object.prototype 的方法, // 用的都是自己构造函数原型上的toString console.log({ name: ‘张三’ }.toString()); // [object Object]
// 使用场景: 用Object.prototype原型里面的toString判断所有数据的真实类型(Object.prototype.toString) console.log(Object.prototype.toString.call(‘张三’)); // [object String] console.log(Object.prototype.toString.call(10)); // [object Number] console.log(Object.prototype.toString.call(true)); // [object Boolean] console.log(Object.prototype.toString.call(new Date())); // [object Date] console.log(Object.prototype.toString.call([1,2,3])); // [object Array]
- **valueOf**:返回当前对象对应的值。- 三大包装类型(还有Date)都有原型成员valueOf,所以基本类型使用valueOf是使用自己包装类型的valueOf原型方法- Object.prototype.valueOf.call(...);才能真正使用Object的valueOf原型方法- 特点:基本类型会得到包装类型的返回值```javascriptlet strObj = new String('demo');console.log(strObj.valueOf());// demo 基本包装类型:返回对应的值let obj = { name: '张三', age: 18 };console.log(obj.valueOf()); // {name:'张三',age:18} 返回对象本身let date = new Date();console.log(date.valueOf()); // 1661678348888 返回时间戳console.log(Object.prototype.valueOf.call(1)); // Number {1}console.log(Object.prototype.valueOf.call('一个字符串')); // String {'一个字符串'}console.log(Object.prototype.valueOf.call(true)); // Boolean {true}console.log(Object.prototype.valueOf.call(date)); // Sun Aug 28 2022 17:21:44 GMT+0800 (中国标准时间)
11.2、静态成员
- assign:将多个对象合并到一个对象中并返回。
- create:创建对象,并设置原型对象
// 该方法可以接收的参数有一下两种// 1. null 创建一个空对象,这个空对象中连最基本的原型对象都没有的Object.create(null)//创建一个空对象// 2. 对象 创建传递进来的对象,并设置该对象的原型对象为当前的参数Object.create({name:"张三"}) // {name:"张三"}参数是原型成员
- is:判断两个参数是否相等,等同于===,注意下面两种特殊的判断即可
console.log(0 === -0); // trueconsole.log(Object.is(-0, 0)); // falseconsole.log(NaN === NaN); // falseconsole.log(Object.is(NaN, NaN)); // true
- values:快速将对象所有属性的值 合并成一个数组并返回 —ES6新增
var obj = {name:'张三',age:20,gender:"男"}console.log(Object.values(obj)); // ['张三', 20, '男']
十二、ES6类的实现—class(掌握)
12.1、class的基本结构
定义及用法:calss关键字定义类,创建构造函数,类名首字母大写
ES6中使用class定义类只是一种语法糖(语法糖能够增加程序的可读性,从而减少程序代码出错的机会的一种语法)
底层最终还是转换成ES5中使用的function类定义类,以及其中的实例成员和原型成员。(简写或者更加语义化)
class Person {// 构造函数constructor(name, age) {// 实例成员this.name = name;this.age = age;}// 原型成员(对象的原型方法,跟构造函数constructor同级)ff(){console.log(this.name);}// 静态成员static jtFf(){return '静态方法';}}let p = new Person('张三',18);p.ff(); // 张三console.log(Person.jtFf()); // 静态方法
12.2、class的继承结构
ES6中class用extends 和 super实现继承。
class Person {constructor(name, age) {this.name = name;this.age = age;}eat() {console.log(this.name + '爱吃盖浇螺蛳面');}}class Student extends Person { // extends 类似原型链继承constructor(name, age, score) {// super必须在子类this之前调用(报错)super(name, age); // super 类似借用构造函数继承 (调用了父类中的构造函数)this.score = score;}getScore() {console.log(this.score);super.eat(); // 使用super关键字调用父类中的构造方法}}var s = new Student('张三', 18, 50);s.eat(); // 张三爱吃盖浇螺蛳面s.getScore(); // 50 张三爱吃盖浇螺蛳面console.log(new Person()); // Person {name: undefined, age: undefined}// js原则上没有类的概念,底层还是原型链,类的概念只是一个语法糖
