js面向对象
学习目标:
- 能够说出什么是面向对象
- 能够说出类和对象的关系
- 能够使用class创建自定义类
- 能够说出什么是继承
js面向对象编程介绍
面向对象编程介绍
两大编程思想
面向过程和面向对象
面向过程编程POP(Process-oriented programming)
面向过程就是分析出解决问题所需要的步骤,然后用函数把这些步骤一步一步实现,使用的时候再一个一个的依次调用就可以了。
举个例子
面向过程:就是按照我们分析好的步骤,按照步骤解决问题
面向对象编程OOP(Object-oriented programming)
面向对象就是把事务分解成一个个对象,然后由对象之间分工与合作。
还是刚才那个那个例子:将大象装进冰箱,面向对象的做法:
先找出对象,并写出这些对象的功能:
- 对象1:大象;功能1:进去
- 对象2:冰箱;功能1:打开,功能2:关闭
最后想把大象装进冰箱,就可以使用大象和冰箱的功能
面向对象:面向对象是以对象的功能来划分问题,而不是步骤
在面向对象程序开发思想中,每一个对象都是功能中心,具有明确地分工。
面向对象编程具有灵活、代码可复用、容易维护和开发的优点,更适合多人合作的大型软件项目。
面向对象的特性:
- 封装性
- 继承性
- 多态性
面向过程和面向对象的对比
面向过程
- 优点:性能比面向对象高,适合更硬件联系很紧密的东西,例如单片机就采用面向过程编程
- 缺点:没有面向对象易维护、易复用、易扩展
面向对象
- 优点:易维护、易复用、易扩展,由于面向对象有封装、继承、多态性的特性,可以设计出低耦合的系统,使系统更加灵活、更加易于维护
- 缺点:性能比面向过程低
总结之总结:用面向过程的方法写出来的程序是一份蛋炒饭,而用面向对象写出来的程序是一份盖浇饭
ES6 中的类和对象
面向对象更贴近我们的实际生活,可以使用面向对象描述现实世界事物,但是事物分为具体的事物和抽象的事物
e.g.
抽象的事物(泛指的):手机
具体的事物(具体的):xx品牌手机
面向对象的思维特点:
- 抽取抽象对象共用的属性和行为组织(封装)成一个类(模板)
- 对类进行实例化,获取类的对象
面向对象编程我们考虑的是有哪些对象,按照面向对象的思维特点,不断的创建对象,使用对象,然后只会对象做事情
对象
现实生活中:万物皆对象,对象是一个具体的事物,看得见摸得着的实物。例如:一本书、一辆汽车、一个人可以是“对象”,一个数据库、一张网页、一个与远程服务器的连接也可以是“对象”。
在JavaScript中,对象是一组无序的相关属性和方法的集合,所有的事物都是对象;例如字符串、数值、数组、函数等。
对象是由属性和方法组成的:
- 属性:事物的特征,在对象中用属性来表示(常使用名词来命名)
- 方法:事物的行为,在对象中用方法来表示(常使用动词来命名)
类 class
类抽象了对象的公共部分,它泛指某一大类(class)
对象 特指某一个,通过类实例化的一个具体的对象
面向对象的思维特点:
- 抽取(抽象)对象共用的属性和行为组织(封装)成一个类(模板)
- 对类进行实例化,获取类的对象
创建类
语法:
class name {
// class body
}
创建实例:
var xx = new name();
注意:类必须使用new 来实例化对象
类中的 constructor 构造函数
constructor()方法是类的构造函数(默认方法),用于传递参数,返回实例对象,通过new命令生成对象实例时,自动调用该方法。如果没有显示定义,类内部会自动给我们创建一个constructor()
类里面的函数不需要加function这个单词,直接写名字加小括号和花括号:constructor(){},定义里要加this
类的声明过程很像之前的构造函数
// 1.创建类 class 创建一个 明星类
class Star {
constructor(uname, age) {
//此处:等号右边的uname,age代表的是两个形参
this.uname = uname;
this.age = age;
}
}
// 2.利用类创建对象 new
var pdd = new Star('胖弟弟', 28);
var lbw = new Star('卢本伟', 31);
console.log(pdd); //胖弟弟
console.log(lbw.uname); // 卢本伟
此处同样可以写作:
// 1.创建类 class 创建一个 明星类
class Star {
constructor(uname, age) {
//此处:等号右边的uname,age代表的是两个形参
this.abc = uname;
this.def = age;
}
}
// 2.利用类创建对象 new
var pdd = new Star('胖弟弟', 28);
var lbw = new Star('卢本伟', 31);
console.log(pdd); //胖弟弟
console.log(lbw.abc); // 卢本伟
// (1)通过class关键字创建类,类名我们还是习惯性定义首字母大写
// (2)类里面有个constructor函数,可以接受传递过来的参数,同时返回实例对象
// (3)constructor函数 只要生成实例时,就会自动调用这个函数,如果我们不写这个函数,类也会自动生成函数
// (4)生成实例时new不能省略
// (5)最后注意语法规范,创建类时,类名后面不要加小括号。生成实例时,类名后面加小括号,构造函数不需要加function
类添加方法
语法:
class Person {
constructor(uname, age) {
this.uname = uname;
this.age = age;
}
say() {
console.log(this.uname + 'hello');
}
}
在后面调用时:
后面调用:
// (1)我们类里面所有的函数不需要写function
// (2)多个函数、方法之间不需要添加逗号分隔
pdd.say(); // pddhello
lbw.say('); // lbwhello
类的继承
继承
现实中的继承:子承父业,比如我们都继承了父亲的姓
程序中的继承:子类可以继承父类的一些属性和方法
语法:
class Father{ //父类
}
class son extends Father{ //子类继承父类
}
一个 小例子:
// 1.类的继承
class Father {
constructor() {
}
money() {
console.log(100);
}
}
class Son extends Father {
}
var son = new Son();
son.money(); // 100
super关键字
super关键字用于访问和调用对象父类上的函数,可以调用父类的构造函数,也可以调用父类的普通函数
class Father {
constructor(x, y) {
this.x = x;
this.y = y;
}
sum() {
console.log(this.x + this.y);
}
}
class Son extends Father {
constructor(x, y) {
// this.x = x;
// this.y = y;
super(x, y); // 调用了父类中的构造函数
}
}
var son = new Son(1, 2);
son.sum();// 若未调用super方法则报错,因为this指向的是父类里的constructor里的形参
// 当调用super方法后 正确输出3
super关键字强化
super关键字用于访问和调用对象父类上的函数,可以调用父类的构造函数,也同样可以调用父类的普通函数
语法:
class Father {
say() {
return 'Iamfather';
}
}
class Son extends Father { //这样子类就继承了父类的属性和方法
say() {
// super.say() super调用父类的方法
return super.say() + '的儿子';
}
}
var damao = new Son();
console.log(damao.say()); // Iamfather的儿子
修改版本:
// super 关键字调用父类普通函数
class Father {
say() {
return 'i am father';
}
}
class Son extends Father {
say() {
// console.log("我是儿子");
console.log(super.say() + "'s son");
// super.say() 就是调用父亲中的普通函数 say()
}
}
var son = new Son();
// son.say();// 我是儿子(未写继承的情况下)
son.say(); //我是儿子(写继承的情况下)就近原则
// 1.继承中,如果实例化子类输出一个方法,先看子类有没有这个方法,如果有就先执行子类的
// 2.继承中,如果子类里面没有,就去查找父亲中有没有这个方法,如果有,就执行父亲的这个方法(就近原则)
在super关键字下子类继承父亲方法
语法:
class Person {
constructor(surname) {
this.surname = surname;
}
}
class Student extends Person { // 子类继承父类
constructor(surname, firstname) {
super(surname); // 调用父类的constructor(surname)
this.firstname = firstname // 定义子类独有的属性
}
}
注意:子类在构造函数中使用super,必须要放到this前面(必须先调用父类的构造方法,然后再使用子类的构造方法)
一个例子:
// 父亲有加法方法
class Father {
constructor(x, y) {
this.x = x;
this.y = y;
}
sum() {
console.log(this.x + this.y);
}
}
// 子类继承父亲的加法方法 同时扩展减法方法
class Son extends Father {
constructor(x, y) {
// super 必须在子类this之前调用
super(x, y);
this.x = x;
this.y = y;
// 利用super 调用父类的构造函数
}
subtract() {
console.log(this.x - this.y);
}
}
var son = new Son(5, 3);
son.subtract();// 未继承时,调用Son里的减法操作,结果是2
son.sum(); //结果是8
ES6中的类和对象三个注意点
注意点1:在ES6中 类 没有变量提升,所以必须先定义类,才能通过类实例化对象
注意点2:类里面的共有属性和方法一定要加this使用
注意点3:类里面的this指向问题、
注意点4:constructor 里面的this指向实例对象,方法里面的this指向这个方法的调用者
<script>
var that;
var _that;
class Star {
constructor(uname, age) {
// constructor 里面的this 指向的是创建的实例对象
that = this;
console.log(this);
this.uname = uname;
this.age = age;
// this.sing();
this.btn = document.querySelector('button');
// sing后面不加小括号,因为不是立马调用
this.btn.onclick = this.sing;
}
sing() {
// 这个sing方法里面的this指向的是btn这个按钮,因为这个按钮调用了这个函数
console.log(this);// 结果是<button>点击</button>,因为this指向btn
console.log(this.uname);//结果是undefined的,因为this指向btn,而btn里面没有uname这个属性
console.log(that.uname);//that里面存储的是constructor里面的this这样返回的就是constructor里面的uname了
}
dance() {
// 这个dance里面的this 指向的是实例对象pdd,因为pdd调用了这个函数
_that = this;
console.log(this); // 结果为Star{...}
}
}
var pdd = new Star('胖弟弟');
pdd.dance();
// pdd.sing();
console.log(that === pdd); // 结果为true,证明this指向的就是pdd这一实例对象
console.log(_that === pdd);// 结果也为true
</script>
面向对象案例
首先:抽取一个共有的对象:tab栏
抽取对象:Tab对象
- 该对象具有切换功能
- 该对象具有添加功能
- 该对象具有删除功能
- 该对象具有修改功能
HTML结构:
<main>
<h4>
Js 面向对象 动态添加标签页
</h4>
<div class="tabsbox" id="tab">
<!-- tab 标签 -->
<nav class="fisrstnav">
<ul>
<li class="liactive"><span>测试1</span><span class="iconfont icon-guanbi"></span></li>
<li><span>测试2</span><span class="iconfont icon-guanbi"></span></li>
<li><span>测试3</span><span class="iconfont icon-guanbi"></span></li>
</ul>
<div class="tabadd">
<span>+</span>
</div>
</nav>
<!-- tab 内容 -->
<div class="tabscon">
<section class="conactive">测试1</section>
<section>测试2</section>
<section>测试3</section>
</div>
</div>
</main>
<script src="js/tab.js"></script>
css:
1.css基础表格样式
* {
margin: 0;
padding: 0;
}
ul li {
list-style: none;
}
main {
width: 960px;
height: 500px;
border-radius: 10px;
margin: 50px auto;
}
main h4 {
height: 100px;
line-height: 100px;
text-align: center;
}
.tabsbox {
width: 900px;
margin: 0 auto;
height: 400px;
border: 1px solid lightsalmon;
position: relative;
}
nav ul {
overflow: hidden;
}
nav ul li {
float: left;
width: 100px;
height: 50px;
line-height: 50px;
text-align: center;
border-right: 1px solid #ccc;
position: relative;
}
nav ul li.liactive {
border-bottom: 2px solid #fff;
z-index: 9;
}
#tab input {
width: 80%;
height: 60%;
}
nav ul li span:last-child {
position: absolute;
user-select: none;
font-size: 12px;
top: -18px;
right: 0;
display: inline-block;
height: 20px;
}
.tabadd {
position: absolute;
/* width: 100px; */
top: 0;
right: 0;
}
.tabadd span {
display: block;
width: 20px;
height: 20px;
line-height: 20px;
text-align: center;
border: 1px solid #ccc;
float: right;
margin: 10px;
user-select: none;
}
.tabscon {
width: 100%;
height: 300px;
position: absolute;
padding: 30px;
top: 50px;
left: 0px;
box-sizing: border-box;
border-top: 1px solid #ccc;
}
.tabscon section,
.tabscon section.conactive {
display: none;
width: 100%;
height: 100%;
}
.tabscon section.conactive {
display: block;
}
2.css字体样式
引用自icomoon
@font-face {font-family: "iconfont";
src: url('./iconfont/iconfont.eot?t=1553960438096'); /* IE9 */
src: url('./iconfont/iconfont.eot?t=1553960438096#iefix') format('embedded-opentype'), /* IE6-IE8 */
url('data:application/x-font-woff2;charset=utf-8;base64,d09GMgABAAAAAAK4AAsAAAAABmwAAAJrAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHEIGVgCCcAp4fwE2AiQDCAsGAAQgBYRtBzAbpQXIrrApw71oi3CCOyzEy8RvE4yIN8TD036/zp03qCYRjaJZNBFFS/gREoRGipQKofjuNrb+9XbTqrmXcqWzfTRDqFqWkhAJzYToaE6LQ7Q30CirRqSKMnj58DdIdrNAdhoTQJa5VGfLrtiAy+lPoAcZdUC57UljTR4TMAo4oL0xiqwYG8YueIHPCdTqYajty/t+bUpmrwvEnUK42lQhLMssVy1UNhzN4kmF6vSQVvMY/T5+HEU1SUXBbti7uBBrx++cgqJULp0GhAgBna5AgSkgE0eN6R1NwTitNt0yAI5VG7wr/8AljmoX7K+zq+tBF1Q8k9JTPWp1AjnJDgCzmM3bU0V31dsvV3M2eC6fHjaGfX/qS7U5Gr58vj6uD0bgxudyrV/OtHHyP+NZnpO1txbktjdY+3FB61+7nxeOzq8niGYnRwT3v3aZxeXf6rrNxl5//49WlEtZUUL1Pj3Bv1EO7MuG2namrCkbvcnApLUJtWpRhv2tzlRLx43kQ7WO2/FW6c5QqDZEZnYKFeosoVK1NdSa5E/XaVM1Ra7BhAEQmk0kjV5QaLbIzG5U6HRRqTkK1DqJtivrjMT1zJaNnIsihAiyQE3JdbszcW0Xiadzdl4d8UO0HSUGNDNXzl2hifYSO5pPjrorgdjUAAavoa5TKDZVUXD3kuuOOzh70fShvUiN2owtNsRxIREIIiATUCYpGO2aqXy/CxEeHcfuaKrLDiGbQ5kcEMsNIK8M5qCmR3mn8RFHOpcECBtlAAwWIZ2OAqV5kQoJXHvShORYBzrDZKhhb3uT8QPlrA3bmsKZV6i89DiTV2o1AAAA') format('woff2'),
url('./iconfont/iconfont.woff?t=1553960438096') format('woff'),
url('./iconfont/iconfont.ttf?t=1553960438096') format('truetype'), /* chrome, firefox, opera, Safari, Android, iOS 4.2+ */
url('./iconfont/iconfont.svg?t=1553960438096#iconfont') format('svg'); /* iOS 4.1- */
}
.iconfont {
font-family: "iconfont" !important;
font-size: 16px;
font-style: normal;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.icon-guanbi:before {
content: "\e676";
}
列出四个功能:
// 1.切换功能
toggleTab() { }
// 2.添加功能
addTab() { }
// 3.删除功能
removeTab() { }
// 4.修改功能
editTab() { }
在类Tab中有一个constructor函数,将#tab传进来作为参数
然后依次获取main(最大的框)、lis(切换栏)、sections(切换栏对应内容)
constructor(id) {
// 获取元素
// 获取主题
this.main = document.querySelector(id);
// 获取所有点击模块li
this.lis = this.main.querySelectorAll('li');
// 获取点击模块对应的内容显示模块section
this.sections = this.main.querySelectorAll('section');
}
写一个init()方法,用来绑定事件,给li增加一个属性:索引号
init() {
//init 初始化操作让相关的元素绑定事件
for (var i = 0; i < this.lis.length; i++) {
// 给li增加一个属性:索引号
this.lis[i].index = i;
this.lis[i].onclick = function () {
console.log(this.index);// 此处的this指向lis[i]
}
}
}
调用方法:
在constructor里面调用init方法可以更加简便
constructor(id) {
this.init();
}
...
new Tab('#tab');
替代:
var tab = new Tab('#tab');
tab.init();
在这里,我们希望一打开页面就能加载init方法,能替代的原因是:只要实例化对象(new了Tab,就会自动执行constructor构造函数里的内容),所以将init方法放到class的constructor构造函数中。
切换功能:toggleTab
this.lis[i].onclick = function () {
console.log(this.index);// 此处的this指向lis[i]
}
替换为:
// toggleTab后面不加小括号因为点击才调用,而不是页面一加载就调用
this.lis[i].onclick = this.toggleTab;
记住一点:
谁调用了这个方法,方法中的this就指向谁。
对于事件绑定,哪个组件绑定了这个方法,方法中的this指向的就是哪个组件。
在toggleTab方法中:
// 1.切换功能
// this指向的是当前的li(也就是lis[i])
toggleTab() {
console.log(this.index);
//排他思想:
that.clearClass();
// 点了谁谁就添加这个类
this.className = 'liactive';
that.sections[this.index].className = 'conactive';
}
其中:clearclass方法
clearClass() {
for (var i = 0; i < this.lis.length; i++) {
this.lis[i].className = '';
this.sections[i].className = '';
}
}
其中,that指的是#tab也就是实例对象,在前面会定义 that 变量:
这样才能保证在section变化时this的正确指向
var that;
constructor(id) {
that = this;
}
点击之后可以自由切换li和对应的section
制作添加功能addTab(){}
- 以前的做法:动态创建元素createElement,但是元素里面内容较多,需要innerHTML赋值,在appendChild追加到父元素里面。
- 现在的高级做法:利用insertAdjacentHTML()可以直接把字符串格式元素添加到父元素中
- appendchild不支持追加字符串的子元素,insertAdjacentHTML支持追加字符串的元素(appendchild是要先createElement才能使用)
insertAdjacentHTML()语法:
element.insertAdjacentHTML(position, text);
参数:
position:
'beforebegin’在element 元素本身的前面
'afterbegin’在element的第一个孩子之前。
'beforeend’在element最后一个孩子之后。
'afterend’在element 元素自身的后面
text:
要解析为HTML或XML并插入到树中的字符串。
1.首先获取按钮:
// 因为要操作加号和点击加号增加选项卡,所以点击
constructor(id) {
this.add = this.main.querySelector('.tabadd');
// li的父元素,这里的ul:first-child是选出第一个ul,在这里由于暂时只有一个ul,所以加不加:first-child结果都是相同的
this.ul = this.main.querySelector('.fisrstnav ul:first-child');
}
2.绑定事件;按钮只有一个所以不用写到for循环里
init() {
//绑定添加功能
this.add.onclick = this.addTab;
}
3.书写添加功能的方法,不使用以前的createELement和insertHTML方法进行创建
使用新方法:insertAdjacentHTML
// 2.添加功能 使用:insertAdjacentHTML()
addTab() {
// (1)创建li元素和section元素
var li = '<li><span>新选项卡</span><span class="iconfont icon-guanbi"></span></li>';
// (2)把这两个元素追加到对应的父元素里面
// 把li元素追加到ul的最后一个孩子的后面
that.ul.insertAdjacentHTML('beforeend', li);
}
在这里仅仅实现了点击加号就实现在li旁增加一个li的效果。
继续完善添加功能
// 首先获取元素.tabscon
constructor(id) {
// section的父元素
this.tabscon = this.main.querySelector('.tabscon');
}
...
addTab() {
// 再次调用清除其他选项卡类名的方法
that.clearClass();
...
var section = '<section class="conactive">新内容</section>';
that.tabscon.insertAdjacentHTML('beforeend', section);
}
最后有个问题:当点击新添加的选项卡发现无法切换选项卡对应的section
上述问题的原因:一开始的时候获取元素时就未获取到新增的元素,自然绑定时也没有绑定上
解决方法:将获取元素单独设置一个方法
然后一点击添加就会重新初始化lis和section的个数
1.将获取li和section单独放到一个方法里:updateNode
// 获取所有的小li和section
updateNode() {
this.lis = this.main.querySelectorAll('li');
this.sections = this.main.querySelectorAll('section');
}
2.将updateNode方法放到init方法里
当调用初始化方法的时候,重新获取时将所有的元素进行绑定。
init() {
// 这里要写在最前面
this.updateNode();
...
}
3.添加功能中增加初始化方法
先将新的li和section创建好了再进行新的获取和绑定,不管获取多少次都会获取到最新的li和section
addTab() {
...
that.init();
}
这样就完全实现好了想要的情况:正确添加并且能够同步切换
制作删除功能removeTab(){}
1.点击关闭按钮获取点击的那个li的索引号,关闭按钮的个数要和li的个数保持相同
获取时:
updateNode() {
// 获取关闭
this.remove = this.main.querySelectorAll('.icon-guanbi');
}
2.绑定删除元素
通过循环的形式给每一个关闭按钮添加点击事件
init() {
for (var i = 0; i < this.lis.length; i++) {
this.remove[i].onclick = this.removeTab;
}
3.获取、绑定之后就可以调用删除功能的方法了
获取当前删除按钮的索引号,因为本身没有,但他的父亲有,所以通过父亲的索引号来获取索引号
removeTab(e) {
// 阻止冒泡,不让父亲触发点击事件
e.stopPropagation();
var index = this.parentNode.index;
console.log(index);
}
通过以上操作,可以获取、绑定事件并且可以成功触发删除事件。
制作删除效果
1.使用remove方法删除li和section
调用init方法获取删除元素后的剩余元素
// 放在removeTab方法中
// 根据索引号删除对应的li和section remove()方法可以直接删除指定的元素
that.lis[index].remove();
that.sections[index].remove();
that.init();
2.使其效果更加完美1:删除某个选定状态的li后让它前一个li被选中
// 放在 init之前
// 当我们删除了选中状态的这个li 的时候,让它的前一个li处于选定状态
// 自动调用我们的点击事件,不需要鼠标触发
// 为防止报错,加一个条件,前者为true才能调用
that.lis[index] && that.lis[--index].click();
2.使其效果更加完美2:
removeTab(e) {
// 当我们删除的不是选中状态的li的时候,原来的选中状态li保持不变
// 放在自动调用点击之前
if (document.querySelector('.liactive')) return;}
制作删除功能editTab(){}
1.获取所有的span
updateNode() {
this.spans = this.main.querySelectorAll('.fisrstnav li span:first-child');
}
2.将获取的所有span进行循环绑定
init() {
for (var i = 0; i < this.lis.length; i++) {
this.spans[i].ondblclick = this.editTab;
}
3.将修改功能的方法进行修改
双击禁用选定文字,和双击修改为文本框
// 4.修改功能
editTab() {
// 双击(只是双击)禁止选定文字
window.getSelection ? window.getSelection().removeAllRanges() : document.selection.empty();
this.innerHTML = '<input type="text" value =' + this.innerHTML + '>';
}
该操作实现了获取绑定和编辑
实现双击修改,失去焦点后保存修改内容
editTab() {
// this 指向的是spans
获取li的内容保存在str里
var str = this.innerHTML;
创建input表单保存在li里
this.innerHTML = '<input type="text" />';
获取上面操作创建的input表单
var input = this.children[0];
使双击创建表单时,里面的内容是原来li里的内容
input.value = str;
input.select();// 让文本框里面的文字处于选定状态
// 当我们离开文本框就把文本框里面的值给span
input.onblur = function () {
this.parentNode.innerHTML = this.value;
}
}
不只是失去焦点更改内容,点击回车也同样达到效果
// 按下回车也可以把文本框里的值给span
input.onkeyup = function (e) {
if (e.keyCode === 13) {
// 手动调用表单失去焦点事件 不需要鼠标离开操作
this.blur();
// 不能这样写,因为blur会和keyup事件冲突,blur事件和click事件就都触发了,由于js是单线程的所以就出现了问题
// this.parentNode.innerHTML = this.value;
}
}
构造函数和原型
学习目标:
- 能够使用构造函数创建对象
- 能够说出原型的作用
- 能够说出访问对象 成员的规则
- 能够使用ES5新增的一些方法
学习目录:
- 构造函数和原型
- 继承
- ES5中的新增方法
构造函数和原型
构造函数和原型概述
在典型的OOP语言中(如Java),都存在类的概念,类就是对象的模板,对象就是类的实例,但在ES6之前,JS中并没有引入类的概念。
ES6,全称ECMAScript 6.0,2015.06发版。但是目前浏览器的JavaScript是ES5版本,大多数高版本的浏览器也支持ES6.不过只实现了ES6的部分特性和功能。
在ES6之前,对象不是基于类创建的,而是用一种称为构建函数来定义对象和他们的特征。
创建对象可以通过以下三种方式:
- 对象字面量
- new Object()
- 自定义构造函数
构造函数
构造函数是一种特殊的函数,主要用来初始化对象,即为对象成员变量赋初始值,它总与new一起使用。我们可以把对象中的一些公共的属性和方法抽取出来,然后封装到这个函数里面。
在JS中,使用构造函数时要注意以下两点:
- 构造函数用于创建某一类对象,其首字母要大写
- 构造函数要和new一起使用才有意义
new在执行时会做四件事情:
- 在内存中创建一个新的空对象。
- 让this指向这个新的对象。
- 执行构造函数里面的代码,给这个新对象添加属性和方法
- 返回这个新对象(所以构造函数里面不需要return)
<script>
// 1.利用 new Object()创建对象
var obj1 = new Object();
// 2.利用 对象字面量创建对象(常用)
var obj2 = {};
// 3.利用构造函数创建对象
function Star(uname, age) {
this.uname = uname;
this.age = age;
this.sing = function () {
console.log('changge');
}
}
var pdd = new Star('pdd', 18);
console.log(pdd);
pdd.sing();
</script>
JavaScript的构造函数中可以添加一些成员,可以在构造函数本身上添加,也可以在构造函数内部的this上添加。通过这两种方式添加的成员,就分别称为静态成员和实例成员
- 静态成员:在构造函数上添加的成员称为静态成员,只能由构造函数本身来访问。
- 实例成员:在构造函数内部创建的对象成员称为实例成员,只能由实例化的对象来访问。
<script>
// 构造函数中的属性和方法称之为成员。成员可以添加
function Star(uname, age) {
this.uname = uname;
this.age = age;
this.sing = function () {
console.log('changge');
}
}
var pdd = new Star('pdd', 18);
// 1.实例成员就是构造函数内部通过this添加的成员 比如uname age sing 就是实例成员
// 实例成员只能通过实例化的对象来访问
console.log(pdd.uname);
pdd.sing();
// console.log(Star.uname)不可以通过构造函数来访问实例成员
// 2.静态成员 在构造函数本身上添加的成员 sex就是静态成员
Star.sex = '男';
// 静态成员 只能通过构造函数来访问
console.log(Star.sex);
// console.log(pdd.sex); 不可以通过对象来访问
</script>
构造函数的问题
构造函数方法很好用,但是存在浪费内存的问题
对于构造函数里的方法,如果创建两个实例对象就会有两片内存空间被创建出来存储同一个函数
如果我们希望所有的对象都使用同一个函数,这样就比较节省内存,那要怎么做呢?
构造函数原型 prototype
构造函数通过原型分配的函数是所有对象所共享的
JavaScript中规定,每一个构造函数都有一个prototype属性(显式原型),指向另一个对象(上一级的__proto__
)。注意这个prototype是一个对象,这个对象的所有属性和方法,都会被构造函数所拥有。
我们可以把那些不变的方法,直接定义在prototype对象上,这样所有对象的实例就可以共享这些方法。
Star.prototype.sing = function () {
console.log('changge');
}
var pdd = new Star('pdd', 18);
var lbw = new Star('卢本伟', 26);
// console.log(pdd.sing === lbw.sing);//结果为false,因为比较的是两个方法的地址
// console.dir(Star); 有个prototype属性 里有{constructor: f}
pdd.sing();
一般情况下,我们的公共属性定义到构造函数里面,公共的方法我们放到原型对象身上
Q&A:
- 原型是什么?一个对象,我们也称为prototype为原型对象。
- 原型的作用是什么?共享方法。
对象原型__proto__
对象都会有一个属性__proto__
指向构造函数的prototype原型对象,之所以我们的对象可以使用构造函数prototype原型对象的属性和方法,就是因为对象有__proto__
原型的存在。
__proto__
对象原型(隐式原型)和原型对象prototype(显式原型)是等价的__proto__
对象原型(隐式原型)的意义就在于为对象的查找机制提供一个方向,或者说一条路线,但是它是一个非标准属性。因此在实际开发中,不可以使用这个属性,它只是内部指向原型对象prototype(显式原型)
- 每个class都有显式原型 prototype
- 每个实例都有隐式原型
__proto__
- 实例的
__proto__
指向对应的class的prototype
<script>
function Star(uname, age) {
this.uname = uname;
this.age = age;
}
Star.prototype.sing = function () {
console.log('changge');
}
var pdd = new Star('pdd', 18);
var lbw = new Star('卢本伟', 26);
pdd.sing();
console.log(pdd);
// 对象身上系统自己添加一个 __proto__这个属性 它指向我们构造函数的原型对象prototype
console.log(pdd.__proto__ === Star.prototype); // 结果为true
// 方法的查找规则:首先先看pdd 身上是否有sing 方法,如果有就执行这个对象上的sing
// 如果没有sing 这个方法,因为有__proto__的存在,就去构造函数原型对象prototype身上去查找
</script>
constructor构造函数
constructor函数就是用来指向该对象引用了哪个构造函数
如果我们修改了原来的原型对象,给原型对象赋值的是一个对象,则必须手动的利用constructor指回原来的构造函数
<script>
function Star(uname, age) {
this.uname = uname;
this.age = age;
}
// 很多情况下,我们需要手动的利用constructor这个属性指回 原来的构造函数
// Star.prototype.sing = function () {
// console.log('changge');
// }
// Star.prototype.movie = function () {
// console.log('演电影');
// }
Star.prototype = {
// 解决方法:
constructor: Star,
sing: function () { console.log('changge'); },
movie: function () { console.log('演电影'); }
}
var pdd = new Star('pdd', 18);
var lbw = new Star('卢本伟', 26);
console.log(Star.prototype);
console.log(pdd.__proto__);
// constructor 主要用于记录该对象引用于哪个构造函数,它可以让原型对象重新指向原来的构造函数
// console.log(Star.prototype.constructor);
// console.log(pdd.__proto__.constructor);
// 结果都是构造函数本身
// 但是更改prototype里面的书写方式:
console.log(Star.prototype.constructor); //结果为:ƒ Object() { [native code] } 而不是构造函数
// 原因是:创建了一个新的对象把原来的prototype对象里的内容全部覆盖了,所以就没有constructor这个属性了
// 解决方法:手动创建一个constructor并且指向Star
console.log(pdd.__proto__.constructor);
</script>
构造函数、实例、原型对象三者之间的关系
每一个构造函数里面都有一个原型对象,构造函数通过prototype指向原型对象,而原型对象里的constructor又指回了构造函数。
原型链
构造函数创建的对象实例的原型__proto__
指向构造函数的原型对象prototype
而这个原型对象prototype也有原型__proto__
它指向Object构造函数的原型对象prototype
同样他也有原型__proto__
,但是它最终指向null,即Object.prototype 没有原型
为查找属性找到一条线路
javascript的成员查找机制(规则)
- 当访问一个对象的属性(包括方法)时,首先查找这个对象自身有没有该属性。
- 如果没有就查找它的原型(也就是
__proto__
指向的prototype原型对象) - 如果还没有就查找原型对象的原型(Object的原型对象)
- 依次类推一直找到Object为止(null)
__proto__
对象原型的意义就在于为对象成员查找机制提供一个方向,或者说一条路线。
如果又有对象实例的相同属性的定义,又有构造函数原型对象里的相同属性定义,执行哪个呢?
var pdd = new Star('pdd', 18);
pdd.sex = 'nan';
Star.prototype.sex = 'nv';
// nan or nv ?
console.log(pdd.sex);
// nan 就近原则
如果只在Object原型对象中有但是在新构造函数和对象实例中没有,依旧可以使用,比如toString方法
原型对象this指向
不管是构造函数中的this还是原型对象中的this指向的都是实例对象
<script>
function Star(uname, age) {
this.uname = uname;
this.age = age;
}
var that;
Star.prototype.sing = function () {
console.log('changge');
that = this;
}
var pdd = new Star('pdd', 18);
// 1. 在构造函数中,里面的this指向的是对象实例
pdd.sing();
console.log(that === pdd);// true 证明this指向的就是pdd这个实例对象
// 2.原型对象函数里面的this 指向的是 实例对象 pdd
</script>
扩展内置对象
可以通过原型对象,对原来的内置对象进行扩展自定义方法,比如给数组增加自定义求偶数和的功能。
注意:数组和字符串内置对象不能给原型对象覆盖操作 Array.prototype = {} , 只能是Array.prototype.xxx = function(){}的方式
<script>
// 原型对象的应用 扩展内置对象方法
// 打印下数组的原型对象,发现没有求和的方法
console.log(Array.prototype);
// 那么我们自创一个求和的方法
Array.prototype.sum = function () {
var sum = 0;
// this指向的是实例对象数组
for (var i = 0; i < this.length; i++) {
sum += this[i];
}
return sum;
}
var arr = [1, 2, 3];
arr.sum();
console.log(arr.sum()); // 6
//扩展内置对象方法后打印一下内置对象看看有没有什么变化
console.log(Array.prototype);// 发现多了一个sum方法
var arr1 = new Array(11, 22, 33);
console.log(arr1.sum()); //66
</script>
继承
ES6之前并没有给我们提供extends继承,我们可以通过构造函数+原型对象模拟实现继承,被称为组合继承
call()
调用这个函数,并且修改函数运行时的this指向
fun.call(thisArg, arg1, arg2, ...)
- thisArg: 当前调用函数this 的指向对象
- arg1, arg2: 传递的其他参数
call方法有两个作用:
1.可以调用函数
2.可以改变this的指向,想指向谁就在括号里的第一个参数写谁
// call 方法
function fn(x, y) {
console.log('hx666');
console.log(this); // this指向window
console.log(x + y);
}
var o = {
name: 'huxiaoxi'
}
// fn();
// 1.call() 可以调用函数
// fn.call();
// 2.call()可以改变函数的this指向 此时这个函数的this 就指向了o这个对象
// fn.call(o);// this指向变为了o
fn.call(o, 1, 2);// this指向变为了o并且成功的将x和y进行了传递
借用构造函数继承父类型属性
ES6之前子构造函数需要继承父构造函数的一些函数,借用父构造函数
原理:调用父构造函数的同时,将父构造函数中的this修改为子构造函数的this
<script>
// 借用父构造函数继承属性
// 1.父构造函数
function Father(uname, age) {
// this 指向父构造函数的对象实例
this.uname = uname;
this.age = age;
}
// 2.子构造函数
function Son(uname, age) {
// this指向子构造函数的对象实例
// 把父构造函数里的this调整为子构造函数里的this
Father.call(this, uname, age);
}
var son = new Son('pdd', 18);
console.log(son);
</script>
借用原型对象继承父类型方法
若想要儿子继承父类的方法:
Son.prototype = Father.prototype;
这样做则会将Father的原型对象中添加只给Son的方法。
方法2:
子原型对象若要使用父原型对象里的方法,不能让子原型对象直接等于父原型对象。
这样的话修改了子原型对象则父原型对象也会跟着改变
改变方法:
让父构造函数创建一个父实例对象,让子原型对象等于父实例对象
Son.prototype = new Father();
因为父实例对象里面有__proto__
,所以它可以拿到父原型对象里的新方法:money;又因为子原型对象指向父实例对象,所以父原型对象里的money方法也同样可以拿到。
而且修改子原型对象里的内容不会影响父原型对象里的内容,因为子原型对象指向的是父实例对象。
最后呢如果利用对象的形式修改了原型对象,别忘了利用constructor指回原来的构造函数
Son.prototype.constructor = Son;
ES5中的新增方法
ES5新增方法概述
ES5中给我们新增了一些方法,可以很方便的操作数组或者字符串,这些方法主要包括:
- 数组方法
- 字符串方法
- 对象方法
数组方法
迭代(遍历)方法:forEach()、map()、filter()、some()、every();
forEach()方法
forEach类似于jQuery里的each方法,也是原生里for循环的加强版
语法:
array.forEach(function(currentValue,index,arr){
....
})
- currentValue:数组当前项的值
- index:数组当前项的索引
- arr: 数组对象本身
<script>
// forEach 迭代(遍历)数组
var arr = [1, 2, 3];
var sum = 0;
arr.forEach(function (value, index, array) {
console.log('meigeshuzuyuansu' + value);// meigeshuzuyuansu1 meigeshuzuyuansu2 meigeshuzuyuansu3
console.log('meigeshuzuyuansudesuoyinhao' + index);// meigeshuzuyuansudesuoyinhao0 meigeshuzuyuansudesuoyinhao1 meigeshuzuyuansudesuoyinhao2
console.log('shuzubenshen' + array); // shuzubenshen1,2,3 打印了三行,内容都一样
sum += value;
})
console.log(sum); // 6
</script>
filter()方法
语法:
array.filter(function(currentValue, index, arr){
...
})
- filter()方法创建一个新的数组,新数组中的元素是通过检查指定数组中符合条件的所有元素,主要用于筛选数组
- 这个方法直接返回一个新数组
- currentValue:数组当前项的值
- index:数组当前项的索引
- arr: 数组对象本身
底层原理就是:将数组中每一个数拿出来和20比较,选出比20大的数(冒泡)
<script>
// filter 筛选数组
var arr = [12, 66, 4, 88, 1];
var newArr = arr.filter(function (value, index, array) {
// return value >= 20;
return value % 2 == 0
});
console.log(newArr); //[12, 66, 4, 88]
</script>
some()方法
语法:
array.some(function(currentValue, index, arr){
...
})
- some()方法用于检测数组中的元素是否满足指定条件,即查找数组中是否有满足条件的元素
- 注意它返回值是布尔值,如果找到这个元素,则返回true,如果找不到就返回false
- 如果找到第一个满足条件的元素,则终止循环,不再继续查找。
- currentValue:数组当前项的值
- index:数组当前项的索引
- arr: 数组对象本身
底层原理:依次比较,若找到就不再查找
<script>
// some 查找数组中是否有满足条件的元素
var arr = [10, 30, 4];
var flag = arr.some(function (value, index) {
return value >= 20;
})
console.log(flag);//true
</script>
总结
1.filter 也是查找满足条件的元素 返回的是一个数组,而且是把所有满足条件的元素返回回来
2.some方法查找满足条件的元素是否存在,返回的是一个布尔值,如果查找到第一个满足条件的元素就停止循环查找
一个案例:
CSS:
<style>
table {
width: 400px;
border: 1px solid #000;
border-collapse: collapse;
margin: 0 auto;
}
td,
th {
border: 1px solid #000;
text-align: center;
}
input {
width: 50px;
}
.search {
width: 600px;
margin: 20px auto;
}
</style>
HTML:
<div class="search">
按照价格查询: <input type="text" class="start"> - <input type="text" class="end"> <button
class="search-price">搜索</button> 按照商品名称查询: <input type="text" class="product"> <button
class="search-pro">查询</button>
</div>
<table>
<thead>
<tr>
<th>id</th>
<th>产品名称</th>
<th>价格</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
基础的未加js的状态
0.写入元素
// 利用新增数组方法操作数据
var data = [{
id: 1,
pname: '小米',
price: 3999
}, {
id: 2,
pname: 'oppo',
price: 999
}, {
id: 3,
pname: '荣耀',
price: 1299
}, {
id: 4,
pname: '华为',
price: 1999
}, ];
1.动态渲染内容,所以先获取其父元素tbody
// 1. 获取相应的元素
var tbody = document.querySelector('tbody');
2.把数据渲染到页面中
先使用forEach方法获取data中的每一个数据,然后创建tr,使用innerHTML方法将数据插入到单元格里面,数据是每一个商品的id价格名字,行里面内容有了,最后再将创建好的行放入tbody里面去
data.forEach(function (value) {
console.log(value);
var tr = document.createElement('tr');
tr.innerHTML = '<td>' + value.id + '</td><td>' + value.pname + '</td><td>' + value.price + '</td>';
tbody.appendChild(tr)
})
渲染成功
制作:根据价格显示数据(filter)
判断条件:大于等于第一个表单里的值,小于等于第二个表单里的值
1.首先获取元素
// 获取相应的元素
var search_price = document.querySelector('.search-price');
var start = document.querySelector('.start');
var end = document.querySelector('.end');
2.筛选所需要的元素
筛选方法:价格要大于最低价格(第一个文本框里的数值),小于最高价格(第二个文本框里的数值)
// 3.根据价格查询商品
// 当我们点击了按钮,就可以根据我们的商品价格去筛选数组里面的对象
search_price.addEventListener('click', function () {
var res = data.filter(function (value) {
return value.price >= start.value && value.price <= end.value
})
})
因为之后一步要进行渲染数据,所以将渲染数据的操作放到一个独立的函数之中
3.封装函数并每次渲染时清空原来的所有数据
// 渲染函数
function setData(mydata) {
tbody.innerHTML = '';
mydata.forEach(function (value) {
// 先清空原来tbody里面的数据
var tr = document.createElement('tr');
tr.innerHTML = '<td>' + value.id + '</td><td>' + value.pname + '</td><td>' + value.price + '</td>';
tbody.appendChild(tr);
})
}
4.渲染
放在点击查询内部
// 把筛选完之后的对象渲染到页面中
search_price.addEventListener('click', function () {
...
setData(jieguo);
})
制作:根据商品名称查找商品
查询数组中唯一一个元素,使用some方法效率更高
1.获取元素
var product = document.querySelector('.product');
var search_pro = document.querySelector('.search-pro');
2.查询
// 4.根据商品名称查找商品
// 如果查询数组中唯一的元素,用some方法更合适,因为当找到这个元素,就不再进行循环,效率更高
search_pro.addEventListener('click', function () {
var arr = []
var jieguo = data.some(function (value) {
if (value.pname === product.value) {
arr.push(value);
return true; // return 后面必须写true
}
})
setData(arr);
})
案例衍生
探讨两个问题:
1.some和forEach的区别
2.为什么some遍历时最后需要return true
var arr = ['red', 'green', 'blue', 'pink'];
// 1.forEach迭代(遍历)
arr.forEach(function (value) {
if (value == 'green') {
console.log('zhaodaogreen le ');
return true; // 在forEach里面return true不会终止迭代
}
console.log(11); // 3个11 red blue pink各打印了一次
})
arr.some(function (value) {
if (value == 'green') {
console.log('zhaodaogreen le ');
return true; // 在some里面遇到return true就会终止迭代 迭代效率更高
}
console.log(11); // 1个11 只因red打印
})
1.some里return true就会终止迭代更加高效,但forEach里却不会终止迭代
同时filter也不会终止迭代
因此:
如果查询数组中唯一的元素,用some方法更合适
字符串方法
trim()方法会从一个字符串的两段删除空白字符
str.trim()
trim()方法并不影响字符串本身,它返回的是一个新的字符串;但是中间的空格不会被去除
<input type="text" name="" id="">
<button>dianji</button>
<div></div>
<script>
// trim方法去除字符串两侧空格
var str = ' pdd ';
// 前有三个空格,后有17个空格
str = str.trim();
console.log(str);// pdd
var ipt = document.querySelector('input');
var btn = document.querySelector('button');
var div = document.querySelector('div');
btn.addEventListener('click', function () {
if (ipt.value.trim() === '') {
alert('请输入内容');
} else {
div.innerHTML = ipt.value.trim();
}
})
</script>
对象方法
1.Object.keys()用于获取独享自身所有的属性
Object.keys(obj)
- 效果类似for…in
- 返回一个由属性名组成的数组
<script>
// 用于获取对象自身所有的属性
var obj = {
id: 1,
pname: 'oppo',
price: 1999,
num: 2000
};
var arr = Object.keys(obj);
console.log(arr); // 0: "id" 1: "pname" 2: "price" 3: "num"
</script>
2.Object.defineProperty()定义对象中新属性或修改原有的属性
Object.defineProperty(obj,prop,descriptor)
- obj:必需,目标对象
- prop:必需,需定义或修改的属性的名字
- descriptor:必需。目标属性所拥有的的特性
第三个参数descriptor:使用对象的形式进行书写
- value:设置属性的值,默认为undefined
- writable: 值是否可以重写。true|false 默认为false
- enumerate:目标属性是否可以被枚举。true|false 默认为false
- configurable:目标属性是否可以被删除或者是否可以再次修改特性true|false 默认为false
// Object.defineProperty() 定义新属性或修改原有的属性
var obj = {
id: 1,
pname: 'oppo',
price: 1999,
};
// //1.这是以前向对象中增加属性的方法
// obj.num = 1000;
// // 以前的修改方法:
// obj.price = 99;
// console.log(obj);
// 2. Object.defineProperty() 定义新属性或修改原有的属性
Object.defineProperty(obj, 'num', {
value: 2000
});
console.log(obj);
Object.defineProperty(obj, 'price', {
value: 546
});
console.log(obj);
Object.defineProperty(obj, 'id', {
//如果值为false 不允许修改这个属性值 默认值也是false
writable: false,
});
obj.id = 2;
console.log(obj);
enumerable:是否可被枚举(就是能不能遍历到,有时需要隐藏)
Object.defineProperty(obj, 'address', {
value: 'zhongguoshandonglanxiangjixiaoxxdanyuan',
// enumerable 如果值为false 则不允许遍历,默认的值是 false
enumerable: false,
// configurable如果为false 则不允许删除这个属性,不允许修改第三个参数里面的特性,默认为false
configurable: false
});
console.log(obj);
console.log(Object.keys(obj)); //"id", "pname", "price"
delete obj.address;
console.log(obj);// address仍然有,因为configurable为false不能被删除