数据类型
浅拷贝和深拷贝
浅拷贝和深拷贝适用于引用数据类型。
- 拷贝基础数据类型时是将变量的值赋给新的变量,这样在修改新的变量时不影响原变量。
let a = 2
let b = a
b = 3
console.log(a, b) //2 3
- 拷贝引用数据类型时分为浅拷贝和深拷贝两种情况。除此之外,将赋值操作也纳入对比。
// 赋值操作是把原引用类型变量保存的地址赋给新变量,两者保存相同的地址,指向同一块内存区域。
let a = [3,[7,8],5]
let b = a
b[0] = 1
b[1] = [6,9]
console.log(a, b) // [1,[6,9],5] [1,[6,9],5]
浅拷贝是把原引用类型变量保存的第一层数据复制给新变量,但如果第一层数据中包括子引用类型,那复制的是这个引用类型保存的地址而非地址指向的内存空间里保存的值。两个变量指向的内存区域是不同的,但第一层数据中如果含有引用类型,这个引用类型指向的内存区域是相同的。
深拷贝是把原引用类型变量保存的所有层数据都复制给新变量。两个变量指向的内存区域是不同的,修改其中一个对另一个毫无影响。
- 如何实现浅拷贝和深拷贝 3.1. 浅拷贝
// 1. Object.assign({}, 被拷贝对象)
let obj1 = {person: {name: 'kobe'}, sports: 'basketball'}
let obj2 = Object.assign({}, obj1)
obj2.sports = 'soccer'
obj2.person.name = 'oneal'
console.log(obj1) // {person: {name: 'oneal'}, sports: 'basketball'}
// 2. 展开运算符...
let obj3 = {...obj1}
obj1.person.name = 'ronaldo'
obj3.sports = 'soccer'
console.log(obj3) // {person: {name:'ronaldo'}, sports: 'soccer'}
// 3. 函数库lodash的clone方法。注意要用npm install lodash --save安装lodash
let _ = require('lodash')
let obj4 = _.clone(obj1)
obj4.sports = 'tennis'
obj4.person.name = 'nadal'
console.log(obj1)
// 4. 手写实现
let obj1hand = {}
for (let k in obj1) {
obj1hand[k] = obj1[k]
}
3.2. 深拷贝
// 1. JSON.parse(JSON.stringify()) 使用JSON.stringify将对象转成JSON字符串,再用JSON.parse把字符串解析成对象,这样对象会开辟新的栈。这样的方法可以实现数组和对象深拷贝,但不能处理函数和正则。
let obj5 = {
a: 1,
b: {
f: { g: 2}
},
c: [1,2,3]
}
let obj6 = JSON.parse(JSON.stringify(obj5))
obj6.b.f.g = 8
console.log(obj6)
// 2. 函数库lodash的cloneDeep方法
let obj7 = _.cloneDeep(obj5)
obj7.b.f.g = 7
console.log(obj7)
console.log(obj5)
// 3. 手写实现。递归思想。
function deepClone(newObj, oldObj) {
for (let k in oldObj) {
let item = oldObj[k]
if (item instanceof Array) {
newObj[k] = []
deepClone(newObj[k], item)
}else if (item instanceof Object) {
newObj[k] = {}
deepClone(newObj[k], item)
}else {
newObj[k] = item
}
}
}
数组高级
方法es5
迭代方法
-
every若每一项都返回true,则返回true
-
filter返回函数调用后值为true的数组成员
array.filter(function(currentValue, index, arr))- filter方法创建一个新的数组,新数组中的元素是通过检查指定数组中符合条件的所有元素,主要用于筛选数组内满足条件的元素并返回一个数组
- 直接返回一个新数组
- currentValue:数组当前的值
- index:数组当前项的索引
- arr:数组对象本身
var arr = [12, 52, 73, 27, 40]; var newArr = arr.filter(function (value, index, array) { return value % 3 === 0; }) console.log(newArr); -
forEach无返回值
array.forEach(function(currentValue, index, arr))- forEach方法用于遍历数组的值和索引
- currentValue:数组当前的值
- index:数组当前项的索引
- arr:数组对象本身
var arr = [1, 2, 3]; var sum = 0; arr.forEach(function (value, index, array) { console.log('每个数组元素' + value); console.log('每个数组元素的索引号' + index); console.log('数组本身' + array); sum += value; }) console.log(sum); -
map返回函数调用后的结果构成的数组
array.some(function(currentValue, index, arr))- map() 方法返回一个新数组,数组中的元素为原始数组元素调用函数处理后的值。
- map() 方法按照原始数组元素顺序依次处理元素。
- 注意: map() 不会对空数组进行检测。
- 注意: map() 不会改变原始数组。
-
some若有一项返回true,则返回true
array.some(function(currentValue, index, arr))- some()方法用于查询数组中是否有满足指定条件的元素,尤其查询数组中个数只有一的元素
- 返回Boolean值。如果有返回true,如果没有返回false
- 如果找到第一个满足条件的元素,则终止循环,不再继续查找
- currentValue:数组当前的值
- index:数组当前项的索引
- arr:数组对象本身
var arr = [23, 65, 78, 17]; var result = arr.some(function (value) { return value % 4 === 0; }) console.log(result);注:一个会多次调用的功能要封装在一个函数内。如果这个函数的效果是在页面上显示数据,且每点击一次显示的数据要变化,为了去除上一次调用显示的数据,需要首先在函数内设置代码的清除,然后再写后续的操作。这样每次调用,都可以首先清除上一次调用在页面上的显示结果。
var arr = ['red', 'green', 'pink', 'yellow']; // 1 forEach arr.forEach(function (value) { if (value === 'green') { console.log('find!'); return true; } console.log('haha!'); }) // 结果打印了三次haha!,说明forEach里面遇到return不会终止迭代 // 2 some arr.some(function (value) { if (value === 'green') { console.log('find!'); return true;// return true 表示找到了元素 } console.log('haha!'); }) // 结果打印了一次haha!,说明some里面遇到return会终止迭代
函数
定义和调用
函数的定义方式
- 函数声明方式function 关键字(命名函数)
function fn() { } - 函数表达式(匿名函数)
var fn = function(){ } - new Function()
var fn = new Function('参数1','参数2','函数体')
- Function里面参数都必须是字符串格式
- 第三种方式执行效率低,不方便书写,使用较少
- 所有函数都是Function构造函数的实例(对象)
- 函数也属于对象(万物皆对象)
函数的调用方式
-
普通函数
function fn(){ console.log('我们终将重逢') } fn(); fn.call(); -
对象的方法
var o = { sayHi = function(){ console.log('捕风的异乡人') } } o.sayHi(); -
构造函数
function Star(){} new Star(); -
绑定事件函数
btn.addEventListener('click',function(){}) -
定时器函数
setInterval(function(){},1000) -
立即执行函数(自动调用)
(function(){})()
this
函数内this的指向
<body>
<button>点击</button>
<script>
// this一般指向函数的调用者
// 1 普通函数的this指向window,因为这里是window.fn()
function fn() {
console.log('我们终将重逢' + this)
}
fn();
// fn.call();
// 2 对象的方法this指向o
var o = {
sayHi: function () {
console.log('捕风的异乡人' + this);
}
}
o.sayHi();
// 3 构造函数this指向实例对象
function Star() {
console.log('干点正事吧巴巴托斯' + this);
}
Star.prototype.sing = function () {
console.log('别想投胎哦'+this);
}
var ldh = new Star();
// 4 绑定事件函数this指向函数的调用者,这里是btn
var btn = document.querySelector('button');
btn.addEventListener('click', function () {
console.log('大丘丘病了' + this);
})
// 5 定时器函数this指向window
setInterval(function () {
console.log('二丘丘瞧' + this);
}, 1000);
// 6 立即执行函数this指向window
(function () {
console.log('三丘丘采药' + this);
})()
</script>
</body>
| 调用方式 | this指向 |
|---|---|
| 普通函数调用 | window |
| 构造函数调用 | 实例对象,原型对象里的方法也指向实例对象 |
| 对象方法调用 | 该方法所属对象 |
| 事件绑定方法 | 触发绑定事件的对象 |
| 定时器函数 | window |
| 立即执行函数 | window |
改变函数的this指向
call()【经常做继承】
call()方法调用一个对象。简单理解为调用函数的方式,但是它可以改变函数的this指向
fun.call(thisArg, arg1, arg2, ...)
// 改变函数内this的指向的三种方法call() apply() bind()
// 1. call()
var o = {
name: 'andy'
}
function fn(a, b) {
console.log(this);
console.log(a * b);
}
// call可以调用函数,还可以改变函数内this的指向
fn.call(window, 2, 6);// this指向window
fn.call(o, 3, 7);// this指向o
// call在实际开发中的主要作用是可以实现继承
function Father(uname, age) {
this.uname = uname;
this.age = age;
}
function Son(uname, age) {
Father.call(this, uname, age);
}
var son = new Son('刘星辰', 10);
console.log(son);
apply()【对数组操作】
apply()方法调用一个对象。简单理解为调用函数的方式,但是它可以改变函数的this指向
fun.apply(thisArg, [argsArray])
- thisArg:在fun函数运行时指定的this
- argsArray:传递的值,必须包含在数组里
- 返回值就是函数的返回值
var o = {
name: 'Eula'
};
function fn(arr) {// 传入的参数必须是(伪)数组
console.log(this);
console.log(arr);// 输出时却不是数组,而是看内容是什么
}
fn.apply(o, ['yanfei']);// this不再指向window,而是指向o
// apply在实际开发中的主要应用是让数组可以借用Math等内置对象
var arr = [3, 75, 24, 96, 47, 28];
var max = Math.max.apply(Math, arr);
console.log(max);
bind()【使用最多,不调用函数,比如改变定时器内部的this指向】
bind()方法不会调用函数。但是它可以改变函数的this指向
fun.bind(thisArg, arg1, arg2])
- thisArg:在fun函数运行时指定的this
- arg1, arg2:传递的其他参数
- 返回由指定的this值和初始化参数改造的原函数拷贝
var o = {
name: '神里绫华'
};
function fn(a, b) {
console.log(this);
console.log(a * b);
}
// bind 改变函数内部的this指向,但不调用
var f = fn.bind(o, 3, 6);
// bind 返回的是原函数改变this之后产生的新函数
f();
<body>
<button>发送验证码</button>
<script>
var o = {
name: '神里绫华'
};
function fn(a, b) {
console.log(this);
console.log(a * b);
}
// bind 改变函数内部的this指向,但不调用
var f = fn.bind(o, 3, 6);
// bind 返回的是原函数改变this之后产生的新函数
f();
var btn = document.querySelector('button');
btn.addEventListener('click', function () {
// var that = this;
this.disabled = true;
setTimeout(function () {
// that.disabled = false;// 定时器里面的this指向window
this.disabled = false;// 这里的this指向window,但在加入bind()后,指向发生了改变
}.bind(this), 3000)// 这里的this在function外,指向btn
})
</script>
</body>
闭包
函数内部可以使用全局变量,函数外部不可以使用函数内的局部变量。当函数执行完毕,作用域内的局部变量会被销毁。
let inVar = 7
let outVar = 10
function fn () {
let inVar = 5
console.log(outVar)
}
console.log(inVar);
fn()
// 函数fn内未定义outVar,但是可以使用函数外部的outVar;函数内外都定义了inVar,但函数外执行时只能用外部的inVar
但使用闭包可以解决这个问题。闭包指有权访问另一个函数作用域中的变量的函数。实际操作时,将被访问的函数定义在发出访问的函数内部。闭包的原理需要理解执行上下文等。
function fn () {
let num = 10
function fun () {
let sum = 20
console.log(num)
console.log(sum)
return sum
}
fun()
return fun
}
let res = fn()
let inner = res()
console.log(inner)
// 在函数fn内定义函数fun并调用,最后将函数fun返回出来。这里在fn函数作用域内的函数fun在调用后访问了fun内部的变量sum和fun外部但fn内部的变量num
// 当调用fn时,将内部函数fun返回出来赋值给res,再调用res即调用了函数fun,将内部的变量sum赋值给inner。这时函数fun内的变量sum就可以被外部使用。
递归
递归函数:函数在内部可以调用本身。
递归函数的效果与循环效果一样。由于递归很容易发生栈溢出错误(stack overflow),所以必须加退出条件return。
function fn(n) {
if (n == 1) {
return 1;
}
return n * fn(n - 1);
}
console.log(fn(4));
var data = [
{
id: 1,
name: '家电',
goods: [
{
id: 11,
gname: '冰箱',
goods: [
{
id: 111,
gname: 'haier'
}, {
id: 112,
gname: 'media'
}
]
},
{
id: 12,
gname: '洗衣机'
}
]
},
{
id: 2,
name: '服饰'
}
];
function getID(json, id) {
var obj = {};
json.forEach(function (item) {
if (item.id == id) {
obj = item;
} else if (item.goods && item.goods.length > 0) {
obj = getID(item.goods, id);
}
})
return obj;
}
console.log(getID(data, 1));
console.log(getID(data, 12));
console.log(getID(data, 111));
流程
异常处理
- throw主动抛出异常
- try指明需要处理的代码段
- catch捕获异常
- finally后期处理
try {
// 尝试执行代码块,throw指定信息
} catch (error) {
// 捕获错误的代码块
} finally {
// 无论try/catch结果如何都会执行的代码块
}
try-catch无法处理异步代码和一些其他场景
构造函数和原型es5
构造函数
在ES6之前,JS中的对象不是基于类创建的,而是用一种称为构造函数的特殊函数来定义对象和它们的特征。
function Star(uname, age, song) {
// uname age sing都是实例成员
this.uname = uname;
this.age = age;
this.sing = function () {
console.log(son)
}
}
let zjl = new Star('周杰伦', 38, '稻香')
构造函数是一种特殊的函数,主要用来初始化对象,即为对象成员变量赋初始值,总与new一起使用。把对象中的公共属性和方法抽取出来,封装到函数中。
- 构造函数用于创建某一类对象,首字母大写
- 构造函数要与new一起用才有意义
new在执行时做的四件事
- 在内存中创建一个新的空对象
- 让this指向这个新对象
- 执行构造函数中的代码,给新对象添加属性和方法
- 返回这个新对象(所以构造函数里不需要return)
成员:构造函数中的属性和方法
- 实例成员:构造函数内部通过this添加的成员。【只能通过实例化的对象访问】
- 静态成员:构造函数身上添加的成员。【只能通过构造函数访问】
构造函数的原型对象prototype
构造函数的问题:在不同对象使用代码相同的函数时,会开辟多个内存空间储存这同一段代码,造成内存的浪费。而构造函数通过原型分配的函数是所有对象共享的,从而解决这个问题。
JS规定,每一个构造函数都有一个prototype属性,指向另一个对象。这个对象的所有属性和方法,都会被构造函数所拥有。因此我们可以把不变的方法直接定义在prototype对象上,这样所有对象的实例就都可以共享这些方法。 这里的prototype称为原型对象,作用是共享方法。
一般实际操作中,公共属性定义到构造函数里,公共方法定义到原型对象里。
function Character(uname, type) {
// 添加公共属性
this.uname = uname;
this.type = type;
/* 不同的对象执行相同的代码,这样写会浪费内存空间
this.skill = function () {
console.log('辅助聚怪');
}
*/
}
// 通常使用构造函数原型处理相同代码的执行问题。添加公共方法
Character.prototype.skill = function () {
console.log('辅助聚怪');
}
var Venti = new Character('Venti', '风');
var Sucrose = new Character('Sucrose', '风');
Venti.skill();
Sucrose.skill();
实例对象的原型__proto__
对象(包括通过构造函数实例化的对象)都会有一个属性__proto__指向构造函数的prototype原型对象,因此对象才可以使用构造函数的prototype原型对象的属性和方法。
实例化后的对象的原型__proto__与构造函数的原型(对象)prototype是等价的。
new 构造函数()得到的Object 的 __proto__ === 构造函数的prototype
方法查找遵循以下规则:对象如果有该方法就执行它上的方法;如果没有,通过__proto__查找构造函数原型对象prototype上是否有这个方法。
原型的constructor
构造函数.prototype和实例化对象的__proto__指向了相同的结果,即原型。 原型中包括添加的方法和属性、一个属性constructor和一个属性__proto__(因为原型也是一个对象)。
这里的constructor称为构造函数,因为它指回构造函数本身。即构造函数.prototype.constructor === 构造函数的结果为true。
// 在向原型中添加共享且不变的属性和方法时,由于数量过多,不适合依次使用下面的方式逐个添加
构造函数.prototype.方法1 = function(){};
构造函数.prototype.方法2 = function(){};
// 而使用
构造函数.prototype = {
方法1:function(){},
方法2:function(){}
}
/* 这样的问题在于第一种方法是添加,不会掩盖掉构造函数.prototype中本来就有的constructor属性;而第二种方法会。因此需要手动添加constructor属性,记录该对象引用于哪个构造函数,让原型对象重新指向原来的构造函数*/
构造函数.prototype = {
constructor:构造函数,
方法1:function(){},
方法2:function(){}
}
原型链
成员查找机制
- 当访问一个对象的属性(或方法)时,首先查找这个对象自身有没有这个属性;
- 如果没有就查找它的原型(也就是
__proto__指向的prototype原型对象); - 如果还没有就查找原型对象的原型(Object的原型对象);
- 依次类推一直找到Object的prototype原型对象.
__proto__为止(即null); __proto__对象原型的意义就在于为对象成员查找机制提供一个方向或路线。
原型对象的this指向
一般情况下,this指向函数的调用者
不论是构造函数还是原型对象,里面的this都指向创建的实例对象。
function Character(uname, type) {
this.uname = uname;
this.type = type;
}
var that;
Character.prototype = {
constructor: Character,
skill: function () {
console.log(this.uname + '辅助聚怪');
that = this;
},
ability: function () {
console.log('挂风增伤');
}
}
var Venti = new Character('Venti', '风');
// 在构造函数中的this指向的是创建的实例对象
Venti.skill();
// 构造函数的原型对象prototype里面的this指向的也是创建的实例对象
console.log(that === Venti);
继承
ES6之前没有提供extends继承,是通过构造函数+原型对象模拟实现继承,称为组合继承。
call()
调用这个函数,并且修改函数运行时的this指向
function.call(thisArg,arg1,arg2)
- thisArg:当前调用函数this的指向对象
- arg1,arg2:传递的其他参数
function Fn(x, y) {
console.log(x+y)
console.log(this)
}
Fn(1,2)// 这时this指向window对象
let obj = { name: 'amber'}
Fn.call(obj, 1, 2)// 这时this指向对象obj
借用父构造函数继承属性
// 父构造函数
function Father(uname, age) {
// this指向父构造函数的对象实例
this.uname = uname;
this.age = age;
}
// 子构造函数
function Son(uname, age) {
// this指向子构造函数的对象实例
Father.call(this, uname, age);
}
var son = new Son('砂糖', 18);
console.log(son);
借用原型对象继承方法
// 父构造函数
function Father(uname, age) {
// this指向父构造函数的对象实例
this.uname = uname;
this.age = age;
}
Father.prototype.money = function () {
console.log(1000);
}
// Son.prototype = Father.prototype 这样直接赋值不可以。会导致子构造函数修改后,父构造函数也跟着修改。因为原型本身也是一个对象,这样赋值,相当于把指向父构造函数的原型对象的地址赋给了子构造函数的原型对象,使得子构造函数的原型对象也使用了父构造函数的原型对象的地址。
Son.prototype = new Father();
// 如果利用对象的形式修改了原型对象,必须利用constructor指回原来的构造函数
Son.prototype.constructor = Son;
// 子构造函数
function Son(uname, age) {
// this指向子构造函数的对象实例
Father.call(this, uname, age);
}
// 子构造函数新添加的方法
Son.prototype.exam = function () {
console.log(99);
}
var son = new Son('砂糖', 18);
console.log(son);
console.log(Father.prototype);
console.log(Son.prototype.constructor);
javascript面向对象es6
面向对象编程
- 面向过程编程Process-oriented Programming:分析出解决问题所需要的步骤,然后用函数把步骤一步一步实现,使用时再一个个的依次调用。性能好,与硬件联系紧密,适合步骤简单明确的程序。
- 面向对象编程Object-oriented Programming:把事务分解成为一个个对象,然后由对象之间分工合作。灵活、易维护、易开发、代码可复用,适合多人合作的大型软件项目。
- 面向过程的重点在于完成一件事的步骤,表现为函数;面向对象的重点在于参与一件事的实体,表现为类和对象。
- 封装性,继承性,多态性
ES6中的类和对象
面向对象的思维特点:
- 抽取对象共用的属性和行为封装成一个类
- 对类进行实例化,获取类的对象
- 创建对象,使用对象,指挥对象做事情
对象:通过类class实例化一个具体的事物。JS中,对象是一组无序的相关属性和方法的集合,所有的事物都是对象。
对象是由属性和方法组成的:
- 属性:事物的特征
- 方法:事物的行为
类:抽象了对象的公共部分,泛指某一大类class。
// 创建类 类名后面没有括号
class className{
// classbody
/* 类里面由constructor构造函数,接收传递来的参数,返回实例对象。
只要new生成实例时就会自动调用 */
// 类里面的构造函数和方法不需要加function
constructor(Fpara1,Fpara2){
// 类中添加属性
this.attr1 = Fpara1;
this.attr2 = Fpara2;
}
// 多个方法之间不需要添加逗号分隔
// 类中添加方法
method(Fpara3){
console.log('XXX'+Fpara3);
}
}
// 创建类的实例即对象 创建对象时类名后面要有括号
var obj = new className(Rpara1,Rpara2);
console.log(obj.method(Rpara3));
类的继承
子类可以继承父类的属性和方法
class Father {// 父类
constructor(x, y) {
this.x = x
this.y = y
}
sum() {
console.log(this.x + this.y)
}
}
class Son extends Father {// extends使子类继承父类的属性和方法
constuctor(x, y) {
super(x, y)
this.x = x
this.y = y
}
sum() {
console.log('Son类的sum')
}
}
class Sports {
constructor(name, fame) {
this.name = name
this.fame = fame
}
icon() {
let str = `${this.fame} is the king of ${this.name}`
return str
}
}
let football = new Sports('football', 'messi')
let strFootball = football.icon()
console.log(strFootball);
class Ball extends Sports {
/* 虽然子类继承了父类,但当调用父类定义的方法时,子类的参数不能传递给父类,也就是说方法里面的this只是指父类的参数。继承并不是简单的复制。要想成功,必须使用super()访问和调用父类上的参数,才能调用父类的构造函数和方法*/
constructor(name, fame, sex) {
/* 先写super,后写this*/
super(name, fame)
this.name = name
this.fame = fame
this.sex = sex
}
icon() {
/*可使用super.方法调用父类的方法*/
let strF = super.icon()
let str = `${this.fame} is the king of ${this.sex}'s ${this.name}`
return [str, strF]
}
}
let tennis = new Ball('tennis', 'federer', 'male')
/*如果子类定义了方法,则使用子类的方法;否则向上查找父类是否定义了该方法,调用父类的方法*/
let strTennis = tennis.icon()[0]
let strTennisF = tennis.icon()[1]
console.log(strTennis);
console.log(strTennisF);
- ES6中没有变量提升,因此必须先定义类,才能通过类实例化对象
- 类里面共有的属性和方法必须加this.使用
- constuctor里面的this指向创建的实例对象;方法里面的this指向该方法的调用者。这不同于ES5的构造函数与原型对象,这两个之间的this都指向创建的实例对象
<body>
<button>点击</button>
<script>
var that;
class Star {
constructor(uname, age) {
// that全局变量,存储了constructor里面的this
that = this;
// constructor 里面的this指向创建的实例对象
this.figname = uname;
this.figage = age;
// this.dance;
this.btn = document.querySelector('button');
this.btn.addEventListener('click', this.sing);
}
sing() {
// sing方法里面的this指向btn按钮,因为btn在上面调用了sing
console.log(that.figname + that.figage);
}
dance() {
// dance方法里面的this指向创建的实例对象
console.log(this.figage);
}
}
var xs = new Star('许嵩', 34);
xs.dance();
</script>
</body>
类的本质
- class的本质还是function【构造函数】
- 类的所有方法都定义在类的prototype上
- 类创建的实例里面也有
__proto__指向类的prototype原型对象 - 对于ES6的类的绝大部分功能,ES5都可以做到。新的class写法只是让对象原型的写法更加清晰、更加面向对象编程的语法。
- 因此,ES6的类其实是语法糖【即更便捷的写法实现相同的功能】。