离职中,有招人的求推荐一下,评论或者私信我邮箱和公司,我发简历给您,最近一直在搞基础的东西,弄了一个持续更新的 github 笔记,可以去看看,诚意之作(本来就是写给自己看的……)链接地址: Front-End-Basics
此篇文章的地址: 面向对象
基础笔记的 github 地址: https://github.com/qiqihaobenben/Front-End-Basics , 可以 watch, 也可以 star。
对象是 JavaScript 的一种数据类型。对象可以看成是属性的无序集合,每个属性都是一个键值对,属性名是字符串,因此可以把对象看成是从字符串到值的映射。这种数据结构在其他语言中称之为 "散列 (hash)"、"字典 (dictionary)"、"关联数组 (associative array)" 等。
原型式继承:对象不仅仅是字符串到值的映射,除了可以保持自有的属性,JavaScript 对象还可以从一个称之为原型的对象继承属性,对象的方法通常是继承的属性,这是 JavaScript 的核心特征。
JavaScript 对象是动态的—可以新增属性也可以删除属性,但是他们常用来模拟静态以及静态类型语言中的 "结构体"
创建对象最简单的方式就是在 JavaScript 代码中使用对象直接量。
- var book = {
- "main title": 'guide', //属性名字里有空格,必须加引号
- "sub-title": 'JS', //属性名字里有连字符,必须加引号
- for: 'development', //for是关键字,不过从ES5开始,作为属性名关键字和保留字可以不加引号
- author: {
- firstname: 'David', //这里的属性名就都没有引号
- surname: 'Flanagan'
- }
- }
注意: 从 ES5 开始,对象直接量中的最后一个属性后的逗号将被忽略。
扩展: [JavaScript 中的关键字和保留字
]( http://blog.mingsixue.com/it/...
new 运算符创建并初始化一个新对象。关键字 new 后跟一个函数调用。这里的函数称做构造函数 (constructor),构造函数用以初始化一个新创建的对象。JavaScript 中的数据类型都包含内置的构造函数。
// 创建一个空对象,和 {} 一样。
- var o = new Object();
// 创建一个空数组,和 [] 一样。
- var arr = new Array();
扩展 1:new
new 是一个一元运算符,专门运算函数的。new 后面调用的函数叫做构造函数,构造函数 new 的过程叫做实例化。
当 new 去调用一个函数 : 这个时候函数中的 this 就指向创建出来的对象, 而且函数的的返回值直接就是 this(隐式返回)
有一个默认惯例就是构造函数的名字首字母大写。
注意:当 return 的时候,如果是后面为简单类型,那么返回值还是这个对象;
如果 return 为对象类型,那么返回的就是 return 后面的这个对象。
扩展 2:基本类型和对象类型(复杂类型)的区别赋值:
基本类型 : 赋值的时候只是值的复制
对象类型 : 赋值不仅是值的复制,而且也是引用的传递(可以理解为内存地址)可以理解为赋址。
比较相等基本类型 : 值相同就可以
对象类型 : 值和引用都相同才行
扩展 3:原型 prototype
每一个 JavaScript 对象 (null 除外) 都和另一个对象相关联,这个对象就是原型,每一个对象都从原型继承属性。
Object.create() 这个方法是 ES5 定义的,它创建一个新对象,其中第一个参数是这个对象的原型。第二个参数是可选参数,用以对对象属性进行进一步描述。
可以通过传入参数 null 创建一个没有原型的新对象,不过这个新对象不会继承任何东西,甚至不包括基础方法。
//o 不会继承任何属性和方法, 空空的。
- var o = Object.create(null);
如果想创建一个普通的空对象,需要传入 Object.prototype
//o 相当于 {}
- var o = Object.create(Object.prototype);
可以通过点 (.) 或方括号 ([]) 运算符来获取和设置属性的值。
- var author = book.author;
- var title = book["main title"];
在 JavaScript 中能用 . 连接的都可以用 []连接。有很多 . 运算符不能用的时候,就需要用 [] 代替。
1、在属性名可变的情况下用 []
- function getAttr(obj, attr) {
- console.log(obj[attr])
- }
2、属性名有空格或者连字符等时用 []
- var title = book["main title"];
delete 运算符可以删除对象的属性。
delete 只是断开属性和宿主对象的联系,而不会去操作属性中的属性,如果删除的属性是个对象,那么这个对象的引用还是存在的。
delete 只能删除自有属性,不能删除继承属性。
- var a = {b:{c:1}};
- var b = a.b;
- console.log(b.c); // 1
- console.log(a.b); // {c:1}
- delete a.b;
- console.log(b.c); // 1
- console.log(a.b); //undefined
返回值为 true
当 delete 表达式删除成功或没有任何副作用 (比如删除不存在的属性),或者 delete 后不是一个属性访问表达式,delete 会返回 true ;
- var a = {b:{c:1}};
- console.log(delete a.b);
- console.log(delete a.b);
- console.log(delete a.toString);
- console.log(delete 1);
- 以上都会打印true
返回值为 false
delete 不能删除那些可配置性为 false 的属性,例如某些内置对象的属性是不可配置的,通过变量声明和函数声明创建的全局对象的属性。
- var a = {};
- Object.defineProperty(a,'b',{
- value:1,
- configurable: false // 设置为不可配置
- })
- console.log(delete a.b)
- console.log(delete Object.prototype)
- var x = 1;
- console.log(delete this.x);
- console.log(delete x)
- 以上打印都为false
in 运算符的左侧是属性名 (字符串), 右侧是对象。如果对象的自有属性或继承属性中包含这个属性则返回 true。
- var a = {b:1};
- console.log('a' in window); // true 声明的全局变量'a'是window的属性
- console.log('b' in a); // true 'b'是a的属性
- console.log('toString' in a); // true a继承了toString属性
- console.log('c' in a); // false 'c'不是a的属性
跟 in 运算符类似的,还可以用 "!==" 判断一个属性是否是 undefined,但是有一种场景只能使用 in 运算符,in 可以区分不存在的属性和存在但值为 undefined 的属性。
- var a = {b:undefined};
- console.log(a.b !== undefined); //false
- console.log(a.c !== undefined); //false
- console.log('b' in a); //true
- console.log('c' in a); //false
对象的 hasOwnProperty() 方法用来检测给定的名字是否是对象的自有属性。对于继承属性它将返回 false
- var a = {b:1};
- console.log(a.hasOwnProperty('b')); //true
- console.log(a.hasOwnProperty('c')); //false
- console.log(a.hasOwnProperty('toString')); //false toString是继承属性
对象的
方法只有检测到是自身属性 (不包括继承的属性) 且这个属性的可枚举性为 true 时它才返回 true。
- propertyIsEnumerable()
- var a = {b:1};
- console.log(a.propertyIsEnumerable('b'));
- console.log(a.propertyIsEnumerable('toString'));
当使用原始类型的值(string、number、boolean),在调用对应属性和方法的时候,内部会自动转成对应的对象。隐式创建的这个对象,就成为包装对象。
基本类型都有自己对应的包装对象 : String Number Boolean
包装对象的特点
隐式创建对象后,可以调用对应的属性和方法
使用后,立马销毁,所以不能给原始类型的值添加属性和方法
其过程举例:str.substring - > new String(1234) - > 找到 String 的 substring -> 将 new String 销毁
两种编程方式:
(1)、面向过程
(2)、面向对象
两者的区别:
面向过程:关注实现过程和每一步的实现细节。
面向对象:关注特征和功能。
通俗点,用对象的思想写代码就是面向对象编程。
1、抽象:抓住核心问题(简单理解为抽出像的部分;将相同或表现与问题相关特征的内容提取出来。)
其核心:抽出、抽离,将相同的部分(可能会维护、会迭代、会扩展)的代码抽离出来形成一类
2、封装:就是将类的属性包装起来,不让外界轻易知道它内部的具体实现;只提供对外接口以供调用
3、继承:从已有对象上继承出新的对象
4、多态:一个对象的不同形态
1、代码的层次结构更清晰
2、更容易复用
3、更容易维护
4、更容易扩展
__proto__对象身上只有 __proto__ 构造函数身上有 prototype 也有 __proto__
属性原型链, 实例对象与原型之间的连接,叫做原型链。
constructor
返回创建实例对象的构造函数的引用, 每个原型都会自动添加 constructor 属性, for..in.. 遍历原型是找不到这个属性的。
- var a = new A();
- console.log(a.constructor == A) //true
hasOwnProperty
可以用来判断某属性是不是这个构造函数的内部属性(不包括继承的)
语法:
返回 Boolean
- obj.hasOwnProperty(prop)
- function A (){
- this.b = 1;
- }
- var a = new A();
- console.log(a.hasOwnProperty('b')); //打印true
- console.log(a.hasOwnProperty('toString')); //toString是继承属性 打印 false
- console.log(a.hasOwnProperty('hasOwnProperty')); //同上,打印false
instanceof
二元运算符, 用来检测一个对象在其原型链中是否存在一个构造函数的 prototype 属性。
语法:
即检测 constructor.prototype 是否存在于参数 object 的原型链上。
- object instanceof constructor
- // 定义构造函数
- function C(){}
- function D(){}
- var o = new C();
- o instanceof C; // true,因为 Object.getPrototypeOf(o) === C.prototype
- o instanceof D; // false,因为 D.prototype不在o的原型链上
- o instanceof Object; // true,因为Object.prototype.isPrototypeOf(o)返回true
- C.prototype instanceof Object // true,同上
toString作用:
返回一个表示该对象的字符串
1、进行数字之间的进制转换
- 例如:var num = 255;
- alert( num.toString(16) ); //结果就是'ff'
2、利用 toString 做类型的判断
- 例如:
- var arr = [];
- alert(Object.prototype.toString.call(arr) == '[object Array]');弹出true Object.prototype.toString.call()得到是类似于'[object Array]''[object Object]'
假如我们有一个对象是狗的原型,这个原型有 "名字" 和 "颜色" 两个属性。
- var Dog = {
- name: '',
- color: ''
- }
根据这个原型对象,我们要生成一个实例对象如下
缺点:
- var hashiqi = {}; //创建空对象,之后根据原型对象的相应属性赋值
- hashiqi.name = 'hashiqi';
- hashiqi.color = 'blackandwhite';
1、如果要生成多个实例对象,要重复写多次。
2、实例和原型之间没有联系。
上面原始模式有一个缺点是要很麻烦的写很多重复的代码,我们可以写一个函数来解决代码重复的问题。
- function Dog(name, color) {
- var obj = {};
- obj.name = name;
- obj.color = color;
- return obj;
- }
- var hashiqi = Dog('hashiqi', 'blackandwhite');
- var jinmao = Dog('jinmao', 'yellow');
这种方式只是解决了代码重复的问题,但是生成的实例跟原型还是没有联系,而且 hashiqi 和 jinmao 也没有联系,不能反映出他们是同一个原型对象的实例。
用来创建对象的函数,叫做构造函数,其实就是一个普通函数,但是默认函数名首字母大写,对构造函数使用 new 运算符,就能生成实例,并且 this 变量会绑定在实例对象上。
- function Dog(name, color) {
- this.name = name;
- this.color = color;
- }
- var hashiqi = new Dog('hashiqi', 'blackandwhite');
- var jinmao = new Dog('jinmao', 'yellow');
- console.log(hashiqi.name); //hashiqi
- console.log(jinmao.name); //jinmao
hasiqi 和 jinmao 有一个共同的构造函数
是 true
- hashiqi.constructor === jinmao.constructor
有以下几种方法可以验证原型对象与实例对象的关系:
缺点:
- hashiqi instanceof Dog; // true
- Object.getPrototypeOf(hashiqi) === Dog.prototype // true
- Dog.prototype.isPrototypeOf(hashiqi) // true
构造函数解决了代码重复和实例与原型之间的联系,但是存在一个浪费内存的问题。比如远行对象有一些不变的属性和通用的方法,这样没生成一个实例,都必须为重复的东西多占一些内存。
扩展
我们可以尝试实现 new 运算符的逻辑如下:
注意:
- function New(func) {
- var obj = {};
- //判断构造函数是否存在原型,如果有实例的__proto__属性就指向构造函数的prototype
- if (func.prototype !== undefined) {
- obj.__proto__ = func.prototype;
- }
- // 模拟出构造函数内部this指向实例的过程,注意,我们会拿到构造函数的返回值
- var res = func.apply(obj, Array.from(arguments).slice(1));
- // 正常构造函数是不需要显式声明返回值的,默认的返回值是生成的实例,但是一旦在构造函数中return 一个不是对象或者函数,就会改变构造函数的默认的返回值,其他的类型是不变的
- if (typeof res === 'object' && res !== null || typeof res === 'function') {
- return res;
- }
- return obj;
- }
- var taidi = New(Dog, 'taidi', 'gray');
正常的构造函数是不需要自己写 return 的,如果写了,当 return 的时候,如果是后面为简单类型,那么返回值还是构造函数生成的实例。如果 return 为对象类型或者函数,那么返回的就是 return 后面的这个对象或者函数。
每一个构造函数都有 prototype 属性,这个属性指向的是一个对象,这个对象的所有属性和方法,都会被构造函数的实例继承。
基于这个属性,我们就可以有选择性的将一些通用的属性和方法定义到 prototype 上,每一个通过 new 生成的实例,都会有一个 __proto__ 属性指向构造函数的原型即 prototype ,这样我们定义到构造函数原型对象的属性和方法,就会被每一个实例访问到,从而变成公用的属性和方法。
注意:当实例对象和原型对象有相同的属性或者方法时,会优先访问实例对象的属性或方法。
- function Dog(name, color) {
- this.name = name;
- this.color = color;
- }
- Dog.prototype.say = function() {
- console.log("汪汪");
- }
- var hashiqi = new Dog('hashiqi', 'blackandwhite');
- var jinmao = new Dog('jinmao', 'yellow');
- hashiqi.say(); // 汪汪
- jinmao.say(); // 汪汪
- console.log(hashiqi.say === jinmao.say); // true
使用 call 或 apply 方法,将父对象的构造函数绑定在子对象上。
- //父类
- function Animal() {
- this.species = '动物';
- }
- //子类
- function Dog(name, color) {
- Animal.call(this);
- this.name = name;
- this.color = color;
- }
- var hashiqi = new Dog('hashiqi', 'blackandwhite');
- console.log(hashiqi.species); //动物
子类的 prototype 指向父类生成实例
- function Animal() {};
- Animal.prototype.species = '动物';
- function Dog(name, color) {
- this.name = name;
- this.color = color;
- }
- Dog.prototype = new Animal();
- //只要是prototype被完全覆盖,都得重写constructor。
- Dog.prototype.constructor = Dog;
- var hashiqi = new Dog('hashiqi', 'blackandwhite');
缺点: 每一次继承都得生成一个父类实例,比较占内存。
利用空对象作为中介
- function Animal() {}
- Animal.prototype.species = '动物';
- function Dog(name, color) {
- this.name = name;
- this.color = color;
- }
- //Middle生成的是空实例(除了__proto__),几乎不占内存
- function Middle() {}
- Middle.prototype = Animal.prototype;
- Dog.prototype = new Middle();
- Dog.prototype.constructor = Dog;
- var hashiqi = new Dog('hashiqi', 'blackandwhite');
- console.log(hashiqi.species);
几个月前在 CSDN 面试的时候,我说了这种继承方式,面试官就纠结这样修改子类的 prototype 不会影响父类么?是真的不会影响的,因为子类的 prototype 是指向 Middle 构造函数生成的实例,如果真的有心要改,得
这么着来改。
- Dog.prototype.__proto__
Object.create()
- function Animal() {}
- Animal.prototype.species = '动物';
- function Dog(name, color) {
- this.name = name;
- this.color = color;
- }
- Dog.prototype = Object.create(Animal.prototype, {
- constructor: {
- value: Dog
- }
- }) var hashiqi = new Dog('hashiqi', 'blackandwhite');
- console.log(hashiqi.species); //动物
浅拷贝
- function Animal() {}
- Animal.prototype.species = '动物';
- function Dog(name, color) {
- this.name = name;
- this.color = color;
- }
- function extend(child, parent) {
- var c = child.prototype;
- var p = parent.prototype;
- for (key in p) {
- c[key] = p[key]
- }
- }
- extend(Dog, Animal);
- var hashiqi = new Dog('hashiqi', 'blackandwhite');
- console.log(hashiqi.species) // 动物
深拷贝
- function deepCopy(parent, child) {
- var child = child || {};
- for (key in parent) {
- if (typeof parent[key] === 'object') {
- child[key] = parent[key].constructor === Array ? [] : {};
- deepCopy(parent[key], child[key])
- } else {
- child[key] = parent[key];
- }
- }
- return child;
- }
上面所说的是 JavaScript 语言的传统方法,通过构造函数,定义并生成新的对象。
ES6 中提供了更接近传统语言的写法,引入了 Class(类) 的概念,通过 class 关键字,可以定义类。
ES6 的类完全可以看成是构造函数的另外一种写法。
- var method = 'say';
- class Dog {
- constructor(name, color) {
- this.name = name;
- this.color = color;
- } //注意,两个属性之间跟对象不同,不要加逗号,并且类的属性名可以使用变量或者表达式,如下
- [method]() {
- console.log('汪汪');
- }
- }
- console.log(typeof Dog); // function 类的数据类型就是函数
- console.log(Dog === Dog.prototype.constructor); // true 类本身就是构造函数
既然是构造函数,所以在使用的时候,也是直接对类使用 new 命令,跟构造函数的用法完全一致。
注意:
- var hashiqi = new Dog('hashiqi', 'blackandwhite');
- console.log(hashiqi.color); // blackandwhite
- //上面采用表达式声明的类的属性可以用一下两种方式调用
- hashiqi[method](); // 汪汪
- hashiqi.say(); // 汪汪
1、先声明定义类,再创建实例,否则会报错
class 不存在变量提升,这一点与 ES5 的构造函数完全不同
- new Dog('hashiqi', 'blackandwhite') class Dog {
- constructor(name, color) {
- this.name = name;
- this.color = color;
- }
- }
- //Uncaught ReferenceError: Dog is not defined
- //上面代码,Dog类使用在前,定义在后,因为ES6不会把类的声明提升到代码头部,所以报错Dog没有定义。
2、必须使用 new 关键字来创建类的实例对象
类的构造函数,不使用 new 是没法调用的,会报错。 这是它跟普通构造函数的一个主要区别,后者不用 new 也可以执行。
- class Dog {
- constructor (name,color) {
- this.name = name;
- this.color = color;
- }
- }
- Dog(); // Uncaught TypeError: Class constructor Dog cannot be invoked without 'new'
3、定义 "类" 的方法的时候,前面不需要加上 function 这个关键字,直接把函数定义放进去了就可以了。并且,方法之间不需要逗号分隔,加了会报错。
constructor 构造函数
构造方法 constructor 是一个类必须要有的方法,默认返回实例对象;创建类的实例对象的时候,会调用此方法来初始化实例对象。如果你没有编写 constructor 方法,执行的时候也会被加上一个默认的空的 constructor 方法。
constructor 方法是必须的,也是唯一的,一个类体不能含有多个 constructor 构造方法。
- class Dog {
- constructor(name, color) {
- this.name = name;
- this.color = color;
- }
- //定义了两个constructor,所以会报错
- constructor() {}
- }
- new Dog('hashiqi', 'blackandwhite')
- //Uncaught SyntaxError: A class may only have one constructor
Class 表达式
与函数一样,类可以使用表达式的形式定义。
- const Hashiqi = class Dog {
- constructor(name, color) {
- this.name = name;
- this.color = color;
- }
- getName() {
- //此处的Dog就是Dog构造函数,在表达式形式中,只能在构造函数内部使用
- console.log(Dog.name);
- }
- }
- var hashiqi = new Hashiqi('hashiqi', 'blackandwhite'); // 真正的类名是Hashiqi
- var jinmao = new Dog('jinmao', 'yellow'); // 会报错,Dog没有定义
通常我们的表达式会写成如下,省略掉类后面的名称
- const Hashiqi = class {
- constructor(name, color) {
- this.name = name;
- this.color = color;
- }
- }
- var hashiqi = new Hashiqi('hashiqi', 'blackandwhite');
实例方法和静态方法
实例化后的对象才可以调用的方法叫做实例方法。
直接使用类名即可访问的方法,称之为 "静态方法"
类相当于实例的原型,所有在类中定义的方法,都会被实例继承。如果在一个方法前,加上 static 关键字,就表示该方法不会被实例继承,而是直接通过类来调用,这就称为 "静态方法"。
静态方法和实例方法不同的是:静态方法的定义需要使用 static 关键字来标识,而实例方法不需要;此外,静态方法通过类名来的调用,而实例方法通过实例对象来调用。
- class Dog {
- constructor(name, color) {
- this.name = name;
- this.color = color;
- }
- static say() {
- console.log('汪汪');
- }
- }
- Dog.say(); //汪汪
extends
类之间可以通过 extends 关键字实现继承,这比 ES5 的通过修改原型链实现继承,要清晰和方便很多。
extends 的继承目标
- class Dog extends Animal {}
extends 关键字后面可以跟多种类型的值,有三种特殊情况
1、子类继承 Object 类
- class A extends Object {}
- console.log(A.__proto__ === Object) //true
- console.log(A.prototype.__proto__ == Object.prototype) //true
- //这种情况下,A其实就是构造函数Object的复制,A的实例就是Object的实例。
2、不存在继承
- class A {}
- console.log(A.__proto__ === Function.prototype) // true
- console.log(A.prototype.__proto__ === Object.prototype) // true
- //这种情况下,A作为一个基类(即不存在任何继承),就是一个普通函数,所以直接继承Funciton.prototype。
- //但是,A调用后返回一个空对象(即Object实例),所以A.prototype.__proto__指向构造函数(Object)的prototype属性。
3、子类继承 null
- class A extends null {}
- console.log(A.__proto__ === Function.prototype) //true
- console.log(A.prototype) //只有一个constructor属性,没有__proto__属性
- 这种情况与第二种情况非常像。A也是一个普通函数,所以直接继承Funciton.prototype。
- 但是,A调用后返回的对象不继承任何方法,所以没有__proto__这属性
super
uper 这个关键字,既可以当作函数使用,也可以当作对象使用。
1、super 作为函数调用时,代表父类的构造函数。作为函数时,super() 只能用在子类的构造函数之中,用在其他地方就会报错。
2、super 作为对象时,在普通方法中,指向父类的原型对象;在静态方法中,指向父类。
注意:
- class Animal {
- constructor(name) {
- this.name = name;
- this.species = '动物';
- }
- say() {
- return this.species;
- }
- }
- class Dog extends Animal {
- constructor(name, color) {
- // 只要是自己在子类中定义constructor,必须调用super方法,否则新建实例会报错
- //super作为函数调用,只能用在子类的constructor中
- super(name);
- this.color = color;
- }
- getInfo() {
- //普通方法中,super指向父类的原型对象
- console.log(super.say() + ': ' + this.name + ',' + this.color);
- }
- }
- var hashiqi = new Dog('hashiqi', 'blackandwhite');
- hashiqi.getInfo() //动物:hashiqi,balckandwhite
1、子类必须在 constructor 方法中调用 super 方法,否则新建实例时会报错。这是因为子类没有自己的 this 对象,而是继承父类的 this 对象,然后对其进行加工。如果不调用 super 方法,子类就得不到 this 对象。
2、在子类的普通方法中,由于 super 指向父类的原型对象,所以定义在父类实例上的方法或属性,是无法通过 super 调用的。
3、使用 super 的时候,必须显式指定是作为函数、还是作为对象使用,否则会报错。
来源: https://segmentfault.com/a/1190000012728341