对象
面向对象的本质是对面向过程的一种封装, 注重结果.
构造函数: new关键字调用的函数,用于创建对象.
(面试题) new在创建这个对象的过程中有哪些作用?
a. 创建空对象
b. this指向这个对象
c. 对象赋值
d.返回这个对象(返回的其实是这个对象的地址,打印地址也就相当于打印了这个对象. )
function Person(name, age, gender) {
// (1) 创建空对象{}
// (2) this指向这个对象 this = {}
// (3) 赋值
this.name = name
this.age = age
this.gender = gender
// (4) 返回这个对象 return this
// return 666 // 无效
// return [10 ,20,30] // 有效
}
let p1 = new Person('小米粒', 20, '女')
console.log(p1);
注意: 在构造函数内部使用return,
如果 return 的是值类型, 则无效, 返回 new 创建的对象.
如果 return 的是引用类型(函数 对象 数组), 则有效, 覆盖 new 创建的对象.
如果在构造函数内部放一个方法,会导致内存浪费, 为什么会出现内存浪费的情况呢?
function Person(name, age) {
this.name = name
this.age = age
this.tryHard = function () {
console.log('努力学习');
}
}
let p1 = new Person('小王', 9)
let p2 = new Person('小李', 9)
console.log(p1.tryHard === p2.tryHard); //false
执行过程图:
之所以会造成内存浪费: 是因为你每创建一个对象,就会在堆空间里又开辟个房间存tryHard里的代码, 严重造成了内存浪费,
console.log(p1.tryHard === p2.tryHard)之所以会错误是因为p1.tryHard 和p2.tryHard比较的是地址,不是值,一个地址是ox1111, 一个地址是0x2222, 所以打印的是false,
记住: 引用类型只比较地址,不比较值。
使用全局函数 解决内存浪费
const tryHard = function () {
console.log('努力学习');
} // 这是个全局函数 无论Person调多少次, 这里只执行一次
function Person(name, age) {
this.name = name
this.age = age
this.tryHard = tryHard // 拷贝 引用类型拷贝的是地址 所以多次创建对象后,每个对象里的tryHard的地址就是那个全局函数tryHard的地址,每个对象找的都是这个全局函数tryHard.减少了资源浪费.
}
let p1 = new Person('小王', 9)
let p2 = new Person('小李', 9)
console.log(p1.tryHard === p2.tryHard); // 相同的地址,指向一个函数
执行过程图:
使用全局函数,之所以能解决内存浪费的原因是: 全局函数只执行一次,
this.tryHard = tryHard ,这里是拷贝,拷贝全局函数(引用类型)的地址, 即后面就算你无论调多少次构造函数进行创建对象, 永远访问的是全局函数tryHard的地址.
这么做的弊端: 造成变量污染(因为一个对象不止一个方法,比如还有学习方法等等,这时候又要创建一个学习的全局函数, 函数变多, 导致变量名增加, 会有重名风险(你的同事假如也起的名字和你一样), 导致变量污染) ,
在工作中过程中,如果你采用这种方式const tryHard = function () {} , 你的同事写的代码里也有这个函数,那么在代码合并的时候,会报错,需要重新费时间去改了,这种错误还好可以看得到;但是你在定义全局函数的时候如果这么写function tryHard() { console.log('你努力学习'); }, 你的同事呢也定义了这个函数名, function tryHard() { console.log('我也努力学习'); },那么在代码合并的时候, 先出现的tryHard()会被后面出现的所覆盖, 没有错误,你压根就不知道代码那里错了, 上面这都是变量污染.
使用对象解决 变量污染 + 内存浪费 使用对象将多个全局函数包起来,
const obj = {
tryHard: function () {
console.log('努力学习');
},
sing: function () {
console.log('唱歌')
}
}
function Person(name, age) {
this.name = name
this.age = age
this.tryHard = obj.tryHard
this.sing = obj.sing
}
let p1 = new Person('小王', 9)
let p2 = new Person('小李', 9)
console.log(p1.sing == p2.sing) //true
弊端: 虽然使用对象解决来了变量污染,但是 obj 自身却成为了唯一的变量污染 后来js作者出手了,发明了原型机制.也就是接下来要介绍的原型对象.
原型对象
任何函数在被创建的时候,系统都会自动帮我们创建一个与之对应的对象,称之为原型对象
原型对象成为了构造函数的一个属性来实现, 即原型prototype, 它以对象的形式存在, 所以又可以称prototype为原型对象. 通过Person.prototype 能找到原型对象.
// 1.构造函数
function Person(name, age) {
this.name = name
this.age = age
}
// 2.原型对象
Person.prototype.eat = function () {
console.log('吃东西'); // 对象赋值 只要对象里没有 相当于增加个属性
}
// 吃方法和学习方法都会加到prototype 原型对象里, 只有一个原型对象.
Person.prototype.learn = function () {
console.log('学习');
}
// 3.实例对象
let p1 = new Person('平安', 18)
let p2 = new Person('宁姚', 17)
console.log(p1);//
console.log(p1.learn());// 学习
执行过程图:
看到这里,有人就该发出疑问了, 构造函数里放一个属性,而这个属性又指向原型对象,那岂不是每次调用构造函数就会创建一个原型对象, 这不还是没解决根本问题吗?
其实可以这么理解: 构造函数也是引用类型, 在构造函数的内存当中, 也可以像对象一样拥有很多小房间, 其中某个小房间专门存代码, 我们每次调用构造函数, 只有这个存代码的小房间会调一次走一次, 你就算调无数次, 其他小房间里的也是只走一次,也是只有一个原型对象 ,用console.log(Person)只能看到走代码的小房间, 用console.dir(Person) 能看到好多小房间.
原型对象解决内存消耗和变量污染的的实质是:是因为这个对象是构造函数的属性,且这个属性只执行一次
仔细看一下下面两行代码的结果图
console.log(p1)
console.log(p1.learn())
对比这两个打印的结果, 发现实例化对象P1里面根本就没有learn这个属性,它为什么能打印出learn里的内容呢? 这是因为实例化对象里有个Prototype这个属性,但是你用这个属性拿不到原型对象, 要通过__proto__才能拿到这个原型对象, 即 p1.__proto__, 所以p1.learn()全部写法是 p1.__proto__.learn() , 用的是原型对象里的learn属性, 但为什么谷歌不直接在实例对象里显示__proto__呢? 是因为__proto__是有争议的, 它从来没有被包括在EcmaScript语言规范中(不是Ecma的标准属性), 但是现代浏览器都实现了它, 所以在开发中强制省略__proto__呀.\
构造函数 原型对象 实例对象三者间的关系:
prototype: 属于构造函数, 指向原型对象
作用: 解决构造函数内存浪费 + 变量污染
__proto__: 属于实例对象, 指向原型对象 作用: 让实例对象直接使用原型对象的成员(函数+属性)
constructor: 属于原型对象, 指向构造函数 作用: 让实例对象知道自己的构造函数是谁
图例关系:
console.log(p1.__proto__.constructor); // 让实例对象知道自己的构造函数谁, 即Person
console.log(p1.__proto__ === Person.prototype); //true 表示构造函数和实例对象指向的是一个原型对象
js作者提前写好的对象, 里面有一些预先定义的方法,我们可直接使用,这类对象叫内置对象. 有Array对象、Date对象、String对象等。
数组对象
操作数组的方法,数组本身没有,用的其实是原型对象里的方法.
先声明个数组, 再来具体介绍数组对象的一些方法.
let arr = [10, 20, 30]
1.连接数组: arr.concat(数组): 连接两个或多个数组, 有返回值, 返回一个新的数组.
arr = arr.concat([40, 50, 60])//arr = [10, 20, 30, 40, 50, 60]
应用场景: 浏览某东网页时,鼠标下拉加载更多资源如图片的时候,会把新数组连接到原来数组的后面.
2.数组拼接字符串: arr.join('分隔符'): 把数组的每个元素拼接成字符串, 有返回值, 返回一个字符串.
['许嵩', '嵩嵩'].join('&')//许嵩&嵩嵩
应用场景: 歌单里有一些歌曲是合唱的,显示多人名字就用到了这个方法.
3.反转数组: arr.reverse() // 这个没有返回值.
4.数组排序: arr.sort()
arr.sort(function (a, b) {
// [10, 20, 30, 40, 50, 60]
return a - b //从小到大
// [60, 50, 40, 30, 20, 10]
//return b-a // 从大到小
})
console.log(arr);
字符串对象
操作字符串的方法,字符串本身没有,用的也是原型对象里的方法.
先声明个字符串,再来具体介绍字符串对象里的一些方法.
let str = '青春正美加油努力'
字符串类似于数组, 也有长度 + 下标
1.返回字符下标: str.inddexOf('字符串')
console.log(str.indexOf('努力')); // 结果为6,注意语法检测的时候是努力二字一起检测,如果你写美力,会检测不到,从而返回-1
有返回值 返回字符串在str中的首字符下标如果存在,则返回首字符下标 如果不存在 则返回固定值-1
应用: 检测字符串在不在str中.
2.字符串分割为字符串数组: str.split('分隔符')
把 str 按照分隔符切割成一个数组 有返回值
应用: 切割 url 网址
let url = 'http://www.baidu.com?username=青春正美&加油努力=123456'
let arr1 = url.split('?')//['http://www.baidu.com', 'username=青春正美&加油努力=123456']
3.截取部分字符串:str.substr(起始位置,截取长度) :
应用场景 : 一般后台返回的数据 不会和前端完全匹配。 有时候需要自己截取一部分
console.log(str.substr(0, 2)); // 从0下标开始, 截取2个字符 青春
4.大小写转换(中文没有大小写:)
console.log('wasd'.toLocaleUpperCase()); //大写WASD
console.log('WASD'.toLocaleLowerCase()); // 小写wasd
了解知识
静态成员: 函数自己的成员
实例成员: 实例对象的成员
后面常用的静态成员:Object.values(对象名)(Object表示内置的构造函数)
作用: 获取对象所有的属性值
Object.keys(对象名)
作用: 获取对象所有的属性名
/* 需求:获取对象所有的属性值 */
let person = {
name: '陈平安',
age: 19,
gender: '男'
}
//以前 : for-in
//对象.属性名 对象[ 变量 ]
//for (let key in person) {
//console.log(person[key])
// }
/* 常用的静态成员 : Object.values(对象名) Object.keys(对象名) */
//返回数组
console.log(Object.values(person)) //['陈平安', 19, '男']
console.log(Object.keys(person)) //['name', 'age', 'gender']