javascript高级部分
Function方法 与 函数式编程
call
语法:call([thisObj[,arg1[, arg2[, [,.argN]]]]])
定义:调用一个对象的一个方法,以另一个对象替换当前对象。
说明:call 方法可以用来代替另一个对象调用一个方法。call 方法可将一个函数的对象上下文从初始的上下文改变为由 thisObj 指定的新对象。
如果没有提供 thisObj 参数,那么 Global 对象被用作 thisObj。
let myName = 'goudan';let myAge = 13; function showMsg(msg){return (msg + '').toLowerCase();}showName(myName); // 'goudan'
这段代码很容易就能看懂,在实际开发工作中,我们会处理不同的数据集合,这时候声明单一变量已经无法满足胃口,需要通过json的形式来存储数据
let person = {name:"kyogre",age:13,hobby:"coding"}let newPerson ={name:'dachui',age:99,hobby:'eat'}function showMsg(msg){return (msg + '').toLowerCase();}showMsg(person.name) // 'kyogre'showMsg(newPerson.name) // 'dachui'
存储数据的方式发生了改变,但是我们处理数据的方式还是那么。。。古老,如果业务复杂一点
function format(msg){return msg+''.toLowerCase();}function show(msg){return '信息的内容是:'+ format(msg);}show(person.name) // '信息内容是:kyogre'show(newPerson.name) // '信息内容是:dachui'
显示的传递上下文对象 (穿参)使得业务越来越复杂,这种叠加会让开发变得冗余和难以解读,bug和闭包横飞
那我们看看通过this如何清晰解决问题
通常this不会指向函数自身,而是调用函数的对象主体。当然,我们可以强制让function自身成为对象主体,这个以后咱们讨论; json本身就是对象,我们是否可以这样:
const person = {name:"kyogre",age:13,hobby:"coding"}const newPerson ={name:'dachui',age:99,hobby:'eat'}function format(){return this.name+''.toLowerCase();}
问题来了,不让穿参这个format中的this指向谁呢? 指向调用format的对象本身,可是调用主体并不明确,可能是person也可能是newPerson,这时回过头看看call的定义吧:调用一个对象的一个方法,以另一个对象替换当前对象。 将函数内部执行上下文对象由原始对象替换为指定对象
const name = '我叫window'; //全局变量 非严格模式下都为 window的属性 window.name;function format(){return (this.name + '').toLowerCase();}format(); //'我叫window';
不要惊慌,本身他就是这样window会作为调用顶级作用域链函数的对象主体;这里的this默认为 window, 用person对象代替window这个默认this主体去执行format会怎么样呢
format.call(person); // kyogre format.call(newPerson);// dachuifunction show(){return '信息的内容是:'+ format.call(this);}show.call(person); // 信息的内容是:kyogre
感觉自己了解了this和call的小明,已经肆无忌惮的笑了起来,这样他就可以从繁重的回调与参数传递中解脱了,并且能够实现方法的初级模块化。
下面可以用call做一些平常的操作
function isArray(object){return Object.prototype.toString.call(object) == '[object Array]';}// 借用Object原型上的toString方法来验证下对象是否是数组?function accumulation(){return [].reduce.call(arguments,(a,b)=>{return a+b}}//让不能使用数组方法的arguments类数组集合使用借用数组的reduce方法return Array.prototype.forEach.call($$('*'),(item)=>{item.style.border = '1px solid red';}//把类数组变成数组对象function fn() {let args = [].slice.call(arguments);console.log(args)}fn(1,2,3,4,5)//[1,2,3,4,5]
使用call可以使类数组具有数组的方法
类数组和数组结构类似,有length方法,但是不能调用数组的方法,借助call可以使用数组的方法
-
获取每一个li的innerText + 10的结果存放到一个数组中
<!DOCTYPE html> <html lang="zh-cn"> <head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>call语法</title> </head> <body><ul><li>1</li><li>2</li><li>3</li><li>4</li></ul><script>const aLi = document.querySelectorAll('ul>li'); //类数组 元素集合 const liArr = [].map.call(aLi, function (item) {return Number(item.innerText) + 10;})console.log(liArr);</script> </body> </html>
-
计算所有参数的和
// 以前不用call只能循环叠加arguments的值 function add() {var count = arguments[0];for (let i = 1, len = arguments.length; i < len; i++) {count += arguments[i];}return count;}//arguments也是类数组,可以借助数组reduce迭代求和 function add() {return [].reduce.call(arguments, function (acc, curr) {return acc + curr;}) } console.log(add(1, 2, 3, 4, 5.5, 6));
-
商品转化成字符串形式 格式化
<!DOCTYPE html> <html lang="zh-cn"> <head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>this 与 call</title> </head> <body><ul id="product"></ul><script>const oUl = document.querySelector('#product');/* 商品转化成字符串形式 格式化商品名称:吸尘器 , 商品价格:$199, 商品库存:999*/const PRODUCT_DATA = [{UID: '018945389743211',productName: '吸尘器',productPrice: '$199',productCount: 999},{UID: '018945389743213',productName: '机器人',productPrice: '$1199',productCount: 222},{UID: '018945389742234',productName: '鼠标',productPrice: '$19',productCount: 111}];function format(idx) {return `商品序号:${idx} 商品名称:${this.productName} , 商品价格:<span>${this.productPrice}</span> , 商品库存:${this.productCount} `}function createTemplate() {let str = ''this.forEach(function (item, idx) {str += `<li>${format.call(item, idx)}</li>`;});return str;}function drawList(str) {oUl.innerHTML = str;}drawList(createTemplate.call(PRODUCT_DATA));// 跳过渲染的行为,直接去改变页面上对元素进行增删改查,是不被允许的// 使用call可以对DOM元素使用数组的方法,但只要涉及实际DOM渲染,是不被允许的sortList();//对商品价格进行排序,但实际排序并没有改变function sortList() {var tempArr = [].slice.call(oUl.children);console.log(oUl.children);tempArr.sort(function (a, b) {const aPrice = parseFloat(a.querySelector('span').innerText);const bPrice = parseFloat(b.querySelector('span').innerText);return aPrice - bPrice;});console.log(tempArr);}</script> </body> </html>
apply
语法:func.apply(thisArg, [argsArray])
call()方法的作用和 apply() 方法类似,区别就是
call()
方法接受的是参数列表,而apply()
方法接受的是一个参数数组。方法.call(替换对象,参数1,参数2,…参数n);
方法.apply(替换对象,[参数1,参数2,参数3…,参数n]);
apply()经常和arguments成对去使用
const person = {fullName: function(city, country) {return this.firstName + " " + this.lastName + "," + city + "," + country;}
}
const person1 = {firstName:"John",lastName: "Doe"
}
person.fullName.apply(person1, ["Oslo", "Norway"]);
//apply()经常和arguments成对去使用
//返回数组中的最大项目
function getMaxNum() {console.log(arguments);//类数组 [参数1,参数2,.....,参数n]return Math.max.apply(null, arguments);
}
console.log(getMaxNum(1, 2, 3, 45, 6, 78, 23, 12, 4, 644, 12343));//12343
Math.max(1, 2, 3, 45, 6, 78, 23, 12, 4, 644, 12343)//12343
//如果是数组参数,可以用apply,当然更好的方法是用解构
Math.max.apply(null, [1,2,3]); // 3
Math.max(...[1,2,3]); // 3//没有concat拼接数组方法之前,可以用push完成concat方法,了解即可
const arr = [1,2,3];
const otherArr = [3,4,5];
arr.push.apply(arr,otherArr);
console.log(arr); // [1, 2, 3, 3, 4, 5]
arr.push(otherArr);
console.log(arr); // [1, 2, 3, [3,4,5]]
案例:自定义任意数量的li内容添加到ul中
<!DOCTYPE html>
<html lang="zh-cn">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>apply</title>
</head>
<body><ul class="box">你好,我是box</ul><script>const oBox = document.querySelector('.box');const ARRAY = ['你好', '我好', '大家好']function appendList(ele) {var args = [].slice.call(arguments, 1);ele.innerHTML = format.apply(null, args);}appendList(oBox, '你好', '我好', '大家好');function format() {return [].reduce.call(arguments, function (acc, curr) {acc += `<li>${curr}</li>`;return acc;}, '');}</script>
</body>
</html>
案例:参数一是函数,后面的参数作为参数一函数的参数
/*fn1参数 cb,...不定参返回值 cb的运行结果(返回值)cb fn1的实参 函数 回调函数参数形参: 不定实参: fn1的参数列表(not:cb) fn1的除了第一位的剩余实参返回值:reduce 归并结果reduce 调用主体对象 call (cb的实参列表对象)返回值: cb的实参列表对象的累加和reduce的返回结果是cb的实参列表累加和cb的实参列表是 fn1的 实参列表.slice(1)*/
function fn1(cb) {let args = [].slice.call(arguments, 1);return cb.apply(null, args);}let result = fn1(function () {return [].reduce.call(arguments, function (acc, curr) {return acc + curr;});}, 2, 3, 4, 5, 6); //20console.log(result);
柯理化函数(currying)
在数学和计算机科学中,柯里化是一种将使用多个参数的一个函数转换成一系列使用一个参数的函数的技术。
柯里化函数的原理是利用闭包机制来实现的
//将以下写法转换成柯里化写法
function fnx(x, y, z) {return x + y + z;
}
//柯里化写法
function fn(x) {return function fn1(y) {x = x + y;return function fn2(z) {return x + z;}}
}
console.log(fn(3)(4)(5));
柯里化函数是利用闭包来实现的
闭包的机制:如果变量还被需要,就不会被回收为持有状态
注意:实际上大多数情况要避免闭包的使用,没什么好处,必须需要局部变量持久化的情况下才用,因为性能开销很大,因为本身垃圾回收机制是浏览器的自我优化,本身就是用来清理垃圾的
//闭包的原理:fn执行会返回fn2,fn2还没有入栈,永远是待执行状态,所以w永远是持有状态,就不会被回收
function fn() {let w = 20;return function fn2() { w++;console.log(w);}
}
fn()()//21 第二次调用已经执行完毕,w被回收
fn()()//21
fn()()//21var x = fn()
x()//21 函数fn2被挂载在x上,永远不会被回收
x()//22
x()//23var x = fn()
var y = fn()
x()//21 x和y是互不干扰互不相通的
y()//21
柯里化函数应用性封装
以上写法只能柯3下,而且一次只能限定传一个参数,无限往下柯里化,并且每次都任何数量的参数写法如下
function currying(fn) {const args = Array.prototype.slice.call(arguments, 1);const inlay = function () {if (arguments.length === 0) {return fn.apply(this, args);}if(arguments.length > 0 ){Array.prototype.push.apply(args, arguments);return inlay;} }return inlay;}function add() {const vals = Array.prototype.slice.call(arguments);return vals.reduce((pre, val) => {return pre + val;});}let newAdd = currying(add, 1, 2, 3);newAdd(4, 5);newAdd(6, 7)(6)(2, 3);console.log(newAdd()); //39
bind
bind()
方法创建一个新的函数,在bind()
被调用时,这个新函数的this
被指定为bind()
的第一个参数,而其余参数将作为新函数的默认参数,供调用时使用。和call apply的区别在于不是主动执行函数的时候进行this的偏转 ,而是在函数声明或者赋值的时候绑定bind
语法:
function.bind(thisArg[, arg1[, arg2[, ...]]])
参数:
- thisArg
调用绑定函数时作为
this参数传递给目标函数的值。 如果使用[
new](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Operators/new)运算符构造绑定函数,则忽略该值。当使用
bind在
setTimeout中创建一个函数(作为回调提供)时,作为
thisArg传递的任何原始值都将转换为
object。如果
bind函数的参数列表为空,或者
thisArg是
null或
undefined,执行作用域的
this将被视为新函数的
thisArg。
- arg1, arg2, …
当目标函数被调用时,被预置入绑定函数的参数列表中的参数。
返回值:
返回一个原函数的拷贝,并拥有指定的 **
this** 值和初始参数。
const OBJ = {petName: 'kyogre',qq: '2971411050',sayHi: function () {console.log(`我是${this.petName} 很高兴认识你`)}
}let sayHi = OBJ.sayHi;
sayHi(); //我是undifined 很高兴认识你 ps: this非严格模式指向了window window.petName不存在let sayHi = OBJ.sayHi.bind(OBJ);
sayHi(); //我是kyogre 很高兴认识你 ps:通过bind强制绑定sayHi函数内部的this指向OBJ
用bind给函数指定对象主体,即使用call也无法强行改变
const MIMI = {age: 1,add: function () {this.age++;console.log(this.age);}}const OUA = {age: 4,add: function () {console.log(this.age);}}let add = MIMI.add.bind(MIMI);add.call(OUA); //2 add已经被永久性绑定给MIMI对象主体了,内部this]永远指向MIMI,即使用call也无法改变
当使用定时器的时候,无法用call或apply,因为call是主动执行函数返回函数值,在定时器中第一个参数要放的是函数体,而不是函数值,所以为了使用定时器时,为了保证内部this,只能使用bind
const MIMI = {age: 1,add: function () {this.age++;console.log(this.age);}}function fn(cb) {// cb && cb.call(MIMI);setInterval(cb.bind(MIMI), 100)//cb.bind(MIMI)返回的时一个新函数,并且新函数永远指向MINI}fn(function () {console.log(this.age);//1})
偏函数的应用:当需要使用某些函数,并且该函数有默认参数的时候,可以使用bind给其设置默认参数
function fn() {return [].reduce.call(arguments, function (acc, curr) {return acc + curr;});}let partial = fn.bind(null, 1,2,3,4);console.log(partial(20));//30
偏函数 (partial)
在计算机科学中,局部应用(偏函数)是指固定一个函数的一些参数,然后产生另一个更小元的函数。(什么是元?元是指函数参数的个数,比如一个带有两个参数的函数被称为二元函数。)
bind()
的另一个最简单的用法是使一个函数拥有预设的初始参数。只要将这些参数(如果有的话)作为bind()
的参数写在this
后面。当绑定函数被调用时,这些参数会被插入到目标函数的参数列表的开始位置,传递给绑定函数的参数会跟在它们后面。一般用于做科学开发,一般我们做小的工具开发的时候,有一些固定的初始值,这些初始值时不变的,函数的初始值虽然可以写到函数内部,但是行为,数据和逻辑一般要分离,就可以用到偏函数,可以扩展函数的多样性
function list() {return Array.prototype.slice.call(arguments);
}function addArguments(arg1, arg2) {return arg1 + arg2
}const list1 = list(1, 2, 3); // [1, 2, 3]const result1 = addArguments(1, 2); // 3// 创建一个函数,它拥有预设参数列表。
const leadingThirtysevenList = list.bind(null, 37);// 创建一个函数,它拥有预设的第一个参数
const addThirtySeven = addArguments.bind(null, 37); const list2 = leadingThirtysevenList();
// [37]const list3 = leadingThirtysevenList(1, 2, 3);
// [37, 1, 2, 3]const result2 = addThirtySeven(5);
// 37 + 5 = 42 const result3 = addThirtySeven(5, 10);
// 37 + 5 = 42 ,第二个参数被忽略
通道函数(compose)
按照顺序
const double = x => x + x;const triple = x => 3 * x;const quarter = x => x / 4;//调用pipe 根据参数顺序 组装返回一个新的函数 function pipe(...funcs) {return function (input) {return funcs.reduce(function (acc, curr) {return curr(acc);}, input);}}//通道函数用箭头函数简化const pipe = (...funcs) => input => funcs.reduce((acc, curr) => curr(acc), input);var result = pipe(quarter, double);console.log(result(10));//5
function toUpperCase(str){return str.toUpperCase(); // 将字符串变成大写}function add(str){return str + '!!!'; // 将字符串拼接}function split(str){return str.split(''); // 将字符串拆分为数组}function reverse(arr){return arr.reverse(); // 将数组逆序}function join(arr){return arr.join('-'); // 将数组按'-'拼接成字符串}function compose(){const args = Array.prototype.slice.call(arguments); // 转换为数组使用下面的方法return function(x){return args.reduceRight(function(result, cb){return cb(result);}, x);}}const f = compose(add, join, reverse, split, toUpperCase);console.log( f('cba') ); // A-B-C!!!
柯理化与偏函数区别
-
柯里化是将一个多参数函数转换成多个单参数函数,也就是将一个 n 元函数转换成 n 个一元函数。
-
局部应用则是固定一个函数的一个或者多个参数,也就是将一个 n 元函数转换成一个 n - x 元函数。
面向对象编程
什么是对象
Everything is object (万物皆对象)
对象到底是什么,我们可以从两次层次来理解。
(1) 对象是单个事物的抽象。
一本书、一辆汽车、一个人都可以是对象,一个数据库、一张网页、一个与远程服务器的连接也可以是对象。当实物被抽象成对象,实物之间的关系就变成了对象之间的关系,从而就可以模拟现实情况,针对对象进行编程。
(2) 对象是一个容器,封装了属性(property)和方法(method)。
属性是对象的状态,方法是对象的行为(完成某种任务)。比如,我们可以把动物抽象为animal对象,使用“属性”记录具体是那一种动物,使用“方法”表示动物的某种行为(奔跑、捕猎、休息等等)。
在实际开发中,对象是一个抽象的概念,可以将其简单理解为:数据集或功能集。
ECMAScript-262 把对象定义为:无序属性的集合,其属性可以包含基本值、对象或者函数。
严格来讲,这就相当于说对象是一组没有特定顺序的值。对象的每个属性或方法都有一个名字,而每个名字都
映射到一个值。
提示:每个对象都是基于一个引用类型创建的,这些类型可以是系统内置的原生类型,也可以是开发人员自定义的类型。
什么是面向对象
面向对象不是新的东西,它只是过程式代码的一种高度封装,目的在于提高代码的开发效率和可维护性。
面向对象编程 —— Object Oriented Programming,简称 OOP ,是一种编程开发思想。
它将真实世界各种复杂的关系,抽象为一个个对象,然后由对象之间的分工与合作,完成对真实世界的模拟。
在面向对象程序开发思想中,每一个对象都是功能中心,具有明确分工,可以完成接受信息、处理数据、发出信息等任务。
因此,面向对象编程具有灵活、代码可复用、高度模块化等特点,容易维护和开发,比起由一系列函数或指令组成的传统的过程式编程(procedural programming),更适合多人合作的大型软件项目。
面向对象与面向过程:
- 面向过程就是亲力亲为,事无巨细,面面俱到,步步紧跟,有条不紊
- 面向对象就是找一个对象,指挥得结果
- 面向对象将执行者转变成指挥者
- 面向对象不是面向过程的替代,而是面向过程的封装
面向对象的特性:
- 封装性
封装,也就是把客观事物封装成抽象的类,并且类可以把自己的数据和方法只让可信的类或者对象操作,对不可信的进行信息隐藏。
- 继承性
继承是指这样一种能力:它可以使用现有类的所有功能,并在无需重新编写原来的类的情况下对这些功能进行扩展。
- 多态性
基于对象所属类的不同,外部对同一个方法的调用,实际执行的逻辑不同。
程序中面向对象的基本体现
在 JavaScript 中,所有数据类型都可以视为对象,当然也可以自定义对象。
自定义的对象数据类型就是面向对象中的类( Class )的概念。
我们以一个例子来说明面向过程和面向对象在程序流程上的不同之处。
假设我们要处理学生的成绩表,为了表示一个学生的成绩,面向过程的程序可以用一个对象表示:
let student1 = { name: 'Michael', score: 98 }
let student2 = { name: 'Bob', score: 81 }
而处理学生成绩可以通过函数实现,比如打印学生的成绩:
function printScore (student) {console.log(`姓名:${student.name} 成绩:${student.score}`);
}
如果采用面向对象的程序设计思想,我们首选思考的不是程序的执行流程,
而是 Student
这种数据类型应该被视为一个对象,这个对象拥有 name
和 score
这两个属性(Property)。
如果要打印一个学生的成绩,首先必须创建出这个学生对应的对象,然后,给对象发一个 printScore
消息,让对象自己把自己的数据打印出来。
抽象数据行为模板(Class):
function Student (name, score) {this.name = namethis.score = score
}Student.prototype.printScore = function () {console.log(`姓名:${this.name} 成绩:${this.score}`);
}
根据模板创建具体实例对象(Instance):
var std1 = new Student('Michael', 98);
var std2 = new Student('Bob', 81);
实例对象具有自己的具体行为(给对象发消息):
std1.printScore() // => 姓名:Michael 成绩:98
std2.printScore() // => 姓名:Bob 成绩 81
面向对象的设计思想是从自然界中来的,因为在自然界中,类(Class)和实例(Instance)的概念是很自然的。
Class 是一种抽象概念,比如我们定义的 Class——Student ,是指学生这个概念,
而实例(Instance)则是一个个具体的 Student ,比如, Michael 和 Bob 是两个具体的 Student 。
所以,面向对象的设计思想是:
- 抽象出 Class
- 根据 Class 创建 Instance
- 指挥 Instance 得结果
面向对象的抽象程度又比函数要高,因为一个 Class 既包含数据,又包含操作数据的方法。
创建对象
简单方式new Object()
我们可以直接通过 new Object()
创建:
const person = new Object()
person.name = 'Jack'
person.age = 18person.sayName = function () {console.log(this.name)
}
每次创建通过 new Object()
比较麻烦,所以可以通过它的简写形式对象字面量来创建:
const person = {name: 'Jack',age: 18,sayName: function () {console.log(this.name)}
}
对于上面的写法固然没有问题,但是假如我们要生成两个 person
实例对象呢?
const person1 = {name: 'Jack',age: 18,sayName: function () {console.log(this.name)}
}const person2 = {name: 'Mike',age: 16,sayName: function () {console.log(this.name)}
}
通过上面的代码我们不难看出,这样写的代码太过冗余,重复性太高。
简单方式的改进:工厂函数
我们可以写一个函数,解决代码重复问题:
function createPerson (name, age) {return {name: name,age: age,sayName: function () {console.log(this.name)}}
}
然后生成实例对象:
let p1 = createPerson('Jack', 18)
let p2 = createPerson('Mike', 18)
这样封装确实爽多了,通过工厂模式我们解决了创建多个相似对象代码冗余的问题,
但却没有解决对象识别的问题(即怎样知道一个对象的类型)。
构造函数
一种更优雅的工厂函数就是下面这样,构造函数:
function Person (name, age) {this.name = namethis.age = agethis.sayName = function () {console.log(this.name)}
}let p1 = new Person('Jack', 18)
p1.sayName() // => Jacklet p2 = new Person('Mike', 23)
p2.sayName() // => Mike
解析构造函数代码的执行
在上面的示例中,Person()
函数取代了 createPerson()
函数,但是实现效果是一样的。
这是为什么呢?
我们注意到,Person()
中的代码与 createPerson()
有以下几点不同之处:
- 没有显示的创建对象
- 直接将属性和方法赋给了
this
对象 - 没有
return
语句 - 函数名使用的是大写的
Person
而要创建 Person
实例,则必须使用 new
操作符。
以这种方式调用构造函数会经历以下 4 个步骤:
- 创建一个新对象
- 将构造函数的作用域赋给新对象(因此 this 就指向了这个新对象)
- 执行构造函数中的代码
- 返回新对象
下面是具体的伪代码:
function Person (name, age) {// 当使用 new 操作符调用 Person() 的时候,实际上这里会先创建一个对象// var instance = {}// 然后让内部的 this 指向 instance 对象// this = instance// 接下来所有针对 this 的操作实际上操作的就是 instancethis.name = namethis.age = agethis.sayName = function () {console.log(this.name)}// 在函数的结尾处会将 this 返回,也就是 instance// return this
}
构造函数和实例对象的关系
使用构造函数的好处不仅仅在于代码的简洁性,更重要的是我们可以识别对象的具体类型了。
在每一个实例对象中的__proto__中同时有一个 constructor
属性,该属性指向创建该实例的构造函数:
console.log(p1.constructor === Person) // => true
console.log(p2.constructor === Person) // => true
console.log(p1.constructor === p2.constructor) // => true
对象的 constructor
属性最初是用来标识对象类型的,
但是,如果要检测对象的类型,还是使用 instanceof
操作符更可靠一些:
console.log(p1 instanceof Person) // => true
console.log(p2 instanceof Person) // => true
总结:
- 构造函数是根据具体的事物抽象出来的抽象模板
- 实例对象是根据抽象的构造函数模板得到的具体实例对象
- 每一个实例对象都具有一个
constructor
属性,指向创建该实例的构造函数- 注意:
constructor
是实例的属性的说法不严谨,具体后面的原型会讲到
- 注意:
- 可以通过实例的
constructor
属性判断实例和构造函数之间的关系- 注意:这种方式不严谨,推荐使用
instanceof
操作符,后面学原型会解释为什么
- 注意:这种方式不严谨,推荐使用
构造函数的问题
使用构造函数带来的最大的好处就是创建对象更方便了,但是其本身也存在一个浪费内存的问题:
function Person (name, age) {this.name = namethis.age = agethis.type = 'human'this.sayHello = function () {console.log('hello ' + this.name)}
}let p1 = new Person('lpz', 18)
let p2 = new Person('Jack', 16)
在该示例中,从表面上好像没什么问题,但是实际上这样做,有一个很大的弊端。
那就是对于每一个实例对象,type
和 sayHello
都是一模一样的内容,
每一次生成一个实例,都必须为重复的内容,多占用一些内存,如果实例对象很多,会造成极大的内存浪费。
console.log(p1.sayHello === p2.sayHello) // => false
对于这种问题我们可以把需要共享的函数定义到构造函数外部:
function sayHello = function () {console.log('hello ' + this.name)
}function Person (name, age) {this.name = namethis.age = agethis.type = 'human'this.sayHello = sayHello
}let p1 = new Person('lpz', 18)
let p2 = new Person('Jack', 16)console.log(p1.sayHello === p2.sayHello) // => true
这样确实可以了,但是如果有多个需要共享的函数的话就会造成全局命名空间冲突的问题。
你肯定想到了可以把多个函数放到一个对象中用来避免全局命名空间冲突的问题:
const fns = {sayHello: function () {console.log('hello ' + this.name)},sayAge: function () {console.log(this.age)}
}function Person (name, age) {this.name = namethis.age = agethis.type = 'human'this.sayHello = fns.sayHellothis.sayAge = fns.sayAge
}let p1 = new Person('lpz', 18)
let p2 = new Person('Jack', 16)console.log(p1.sayHello === p2.sayHello) // => true
console.log(p1.sayAge === p2.sayAge) // => true/*可以使用prototype原型来解决以上问题prototype 原型 NDA原型就是用来承载DNA中的行为的载体原型是一个对象1. 解决了行为方法作为属性 在实例化的时候会被拷贝而导致内存紧张2. 解决了命名空间的冲突和方法与对象的从属关系
*/
至此,我们利用自己的方式基本上解决了构造函数的内存浪费问题。
但是代码看起来还是那么的格格不入,那有没有更好的方式呢?
原型
内容引导:
- 使用 prototype 原型对象解决构造函数的问题
- 分析 构造函数、prototype 原型对象、实例对象 三者之间的关系
- 属性成员搜索原则:原型链
- 实例对象读写原型对象中的成员
- 原型对象的简写形式
- 原生对象的原型
- Object
- Array
- String
- …
- 原型对象的问题
- 构造的函数和原型对象使用建议
更好的解决方案: prototype
Javascript 规定,每一个构造函数都有一个 prototype
属性,指向另一个对象。
这个对象的所有属性和方法,都会被构造函数的实例继承。
这也就意味着,我们可以把所有对象实例需要共享的属性和方法直接定义在 prototype
对象上。
function Person (name, age) {this.name = namethis.age = age
}console.log(Person.prototype)Person.prototype.type = 'human'Person.prototype.sayName = function () {console.log(this.name)
}let p1 = new Person(...)
let p2 = new Person(...)console.log(p1.sayName === p2.sayName) // => true
这时所有实例的 type
属性和 sayName()
方法,
其实都是同一个内存地址,指向 prototype
对象,因此就提高了运行效率。
构造函数、实例、原型三者之间的关系
任何函数都具有一个 prototype
属性,该属性是一个对象。
function F () {}
console.log(F.prototype) // => objectF.prototype.sayHi = function () {console.log('hi!')
}
构造函数的 prototype
对象默认都有一个 constructor
属性,指向 prototype
对象所在函数。
console.log(F.constructor === F) // => true
通过构造函数得到的实例对象内部会包含一个指向构造函数的 prototype
对象的指针 __proto__
。
var instance = new F()
console.log(instance.__proto__ === F.prototype) // => true
`__proto__` 是非标准属性。
实例对象可以直接访问原型对象成员。
instance.sayHi() // => hi!
总结:
- 任何函数都具有一个
prototype
属性,该属性是一个对象 - 构造函数的
prototype
对象默认都有一个constructor
属性,指向prototype
对象所在函数
//函数,原型prototype和构造器constructor相当于以下关系,是伪代码,实际更复杂const fn = {prototype: {constructor: fn}}
- 通过构造函数得到的实例对象内部会包含一个指向构造函数的
prototype
对象的指针__proto__
- 所有实例都直接或间接继承了原型对象的成员
属性成员的搜索原则:原型链
了解了 构造函数-实例-原型对象 三者之间的关系后,接下来我们来解释一下为什么实例对象可以访问原型对象中的成员。
每当代码读取某个对象的某个属性时,都会执行一次搜索,目标是具有给定名字的属性
- 搜索首先从对象实例本身开始
- 如果在实例中找到了具有给定名字的属性,则返回该属性的值
- 如果没有找到,则继续搜索指针指向的原型对象,在原型对象中查找具有给定名字的属性
- 如果在原型对象中找到了这个属性,则返回该属性的值
也就是说,在我们调用 person1.sayName()
的时候,会先后执行两次搜索:
- 首先,解析器会问:“实例 person1 有 sayName 属性吗?”答:“没有。
- ”然后,它继续搜索,再问:“ person1 的原型有 sayName 属性吗?”答:“有。
- ”于是,它就读取那个保存在原型对象中的函数。
- 当我们调用 person2.sayName() 时,将会重现相同的搜索过程,得到相同的结果。
而这正是多个对象实例共享原型所保存的属性和方法的基本原理。
总结:
- 先在自己身上找,找到即返回
- 自己身上找不到,则沿着原型链向上查找,找到即返回
- 如果一直到原型链的末端还没有找到,则返回
undefined
实例对象读写原型对象成员
读取:
- 先在自己身上找,找到即返回
- 自己身上找不到,则沿着原型链向上查找,找到即返回
- 如果一直到原型链的末端还没有找到,则返回
undefined
值类型成员写入(实例对象.值类型成员 = xx
):
- 当实例期望重写原型对象中的某个普通数据成员时实际上会把该成员添加到自己身上
- 也就是说该行为实际上会屏蔽掉对原型对象成员的访问
引用类型成员写入(实例对象.引用类型成员 = xx
):
- 同上
复杂类型修改(实例对象.成员.xx = xx
):
- 同样会先在自己身上找该成员,如果自己身上找到则直接修改
- 如果自己身上找不到,则沿着原型链继续查找,如果找到则修改
- 如果一直到原型链的末端还没有找到该成员,则报错(
实例对象.undefined.xx = xx
)
更简单的原型语法
我们注意到,前面例子中每添加一个属性和方法就要敲一遍 Person.prototype
。
为减少不必要的输入,更常见的做法是用一个包含所有属性和方法的对象字面量来重写整个原型对象:
function Person (name, age) {this.name = namethis.age = age
}Person.prototype = {type: 'human',sayHello: function () {console.log('我叫' + this.name + ',我今年' + this.age + '岁了')}
}
在该示例中,我们将 Person.prototype
重置到了一个新的对象。
这样做的好处就是为 Person.prototype
添加成员简单了,但是也会带来一个问题,那就是原型对象丢失了 constructor
成员。
所以,我们为了保持 constructor
的指向正确,建议的写法是:
function Person (name, age) {this.name = namethis.age = age
}Person.prototype = {constructor: Person, // => 手动将 constructor 指向正确的构造函数type: 'human',sayHello: function () {console.log('我叫' + this.name + ',我今年' + this.age + '岁了')}
}
原生对象的原型
所有函数都有 prototype 属性对象。
- Object.prototype
- Function.prototype
- Array.prototype
- String.prototype
- Number.prototype
- Date.prototype
- …
原型对象的问题
- 共享数组
- 共享对象
如果真的希望可以被实例对象之间共享和修改这些共享数据那就不是问题。但是如果不希望实例之间共享和修改这些共享数据则就是问题。
一个更好的建议是,最好不要让实例之间互相共享这些数组或者对象成员,一旦修改的话会导致数据的走向很不明确而且难以维护。
原型对象使用建议
- 私有成员(一般就是非函数成员)放到构造函数中
- 共享成员(一般就是函数)放到原型对象中
- 如果重置了
prototype
记得修正constructor
的指向
prototype 与 __proto__
prototype
每个函数都有一个prototype属性,该属性是一个指针,指向一个对象(构造函数的原型对象) ,这个对象包含所有实例共享的属性和方法。原型对象都有一个constructor
属性,这个属性指向所关联的构造函数。使用这个对象的好处就是可以让所有实例对象共享它所拥有的属性和方法。这个属性只用js中的类(或者说能够作为构造函数的对象)才会有。
__proto__
每个实例对象都有一个proto属性,用于指向构造函数的原型对象(protitype
)。__proto__属性是在调用构造函数创建实例对象时产生的。该属性存在于实例和构造函数的原型对象之间,而不是存在于实例与构造函数之间。
function Person(name, age, job){ this.name = name;this.age = age;this.job = job;this.sayName = function(){console.log(this.name);}; // 与声明函数在逻辑上是等价的
}
let person1 = new Person("Nicholas", 29, "Software Engineer");
console.log(person1);
console.log(Person);
console.log(person1.prototype);//undefined
console.log(person1.__proto__);
console.log(Person.prototype);
console.log(person1.__proto__ === Person.prototype);//true1、调用构造函数创建的实例对象的prototype属性为"undefined",构造函数的prototype是一个对象。
2、proto属性是在调用构造函数创建实例对象时产生的。
3、调用构造函数创建的实例对象的proto属性指向构造函数的prototype,本质上就是继承构造函数的原型属性。
4、在默认情况下,所有原型对象都会自动获得一个constructor(构造函数)属性,这个属性包含一个指向prototype属性所在函数的指针。
__proto__
:是对象
就会有这个属性(强调是对象);函数
也是对象,那么函数也有这个属性咯,它指向构造函数
的原型对象;prototype
:是函数
都会有这个属性(强调是函数),普通对象
是没有这个属性的(JS 里面,一切皆为对象,所以这里的普通对象
不包括函数对象
).它是构造函数的原型对象;constructor
:这是原型对象
上的一个指向构造函数
的属性。
总结:
- 每一个对象都有
__proto__
属性,__proto__
==>Object.prototype
(Object 构造函数的原型对象); - 每个函数都
__proto__
和prototype
属性; - 每个
原型对象
都有constructor
和__proto__
属性,其中constructor
指回’构造函数’, 而__proto__
指向Object.prototype
; object
是有对象的祖先,所有对象都可以通过__proto__
属性找到它;Function
是所有函数的祖先,所有函数都可以通过__proto__
属性找到它;- 每个函数都有一个
prototype
,由于prototype
是一个对象,指向了构造函数的原型对象 - 对象的
__proto__
属性指向原型
,__proto__
将对象和原型链接起来组成了原型链
isPrototypeOf()
当现实中无法访问到proto,但可以通过 isPrototypeOf()方法来确定对象之间是否存在这种关系。
alert(Person.prototype.isPrototypeOf(person1)); //true
alert(Person.prototype.isPrototypeOf(person2)); //true
Object.getPrototypeOf()
在所有支持的实现中,这个方法返回proto的值。例如:
//这里的person1是下面实例
alert(Object.getPrototypeOf(person1) == Person.prototype); //true
alert(Object.getPrototypeOf(person1).name); //"Nicholas" person1
注意: 虽然可以通过对象实例访问保存在原型中的值,但却不能通过对象实例重写原型中的值。如果我们在实例中添加了一个属性,而该属性与实例原型中的一个属性同名,那我们就在实例中创建该属性,该属性将会屏蔽原型中的那个属性。
hasOwnProperty()
可以检测一个属性是存在于实例中,还是存在于原型中。返回值为true表示该属性存在实例对象中,其他情况都为false。
function Pig(name = '佩奇', age = 1) {this.name = name;this.age = age;}Pig.prototype.sex = 1;let pig = new Pig('佩奇', 2);for (let key in pig) {if (pig.hasOwnProperty(key)) {//判断key是否是pig的自身属性而不是原型上属性 有这条判断就不会访问到sex属性console.log(key, pig[key]); }}
in 操作符
无论该属性存在于实例中还是原型中。只要存在对象中,都会返回true。但是可以同时使用 hasOwnProperty()方法和 in 操作符,就可以确定该属性到底是存在于对象中,还是存在于原型中。
function Person(name = '甲', age = 1, job = '无业'){ this.name = name;this.age = age;this.job = job;this.sayName = function(){console.log(this.name);}; // 与声明函数在逻辑上是等价的
}var person1 = new Person();
var person2 = new Person();
console.log(person1.hasOwnProperty("name")); //true
console.log("name" in person1); //true
person1.name = "Greg";
console.log(person1.name); //"Greg" —— 来自实例
console.log(person1.hasOwnProperty("name")); //true
console.log("name" in person1); //true
console.log(person2.name); //"甲" —— 来自原型
console.log(person2.hasOwnProperty("name")); //true
console.log("name" in person2); //true
delete person1.name;
console.log(person1.name); //undefined 被删除了
console.log(person1.hasOwnProperty("name")); //false
console.log("name" in person1); //false
用in或object[key]判断一个对象有没有这个属性两个方法的区别
let obj = {a: 1,b: undefined,c: 0}console.log(!!obj['a'], 'a' in obj);//true trueconsole.log(!!obj['b'], 'b' in obj);//false true 存在隐式转换,所以建议用in来判断一个对象有没有存在的属性console.log(!!obj['c'], 'c' in obj);//false true
原型链与继承
对于使用过基于类的语言 (如 Java 或 C++) 的开发人员来说,JavaScript 有点令人困惑,因为它是动态的,并且本身不提供一个
class
实现。(在 ES2015/ES6 中引入了class
关键字,但那只是语法糖,JavaScript 仍然是基于原型的)。当谈到继承时,JavaScript 只有一种结构:对象。每个实例对象( object )都有一个私有属性(称之为 proto )指向它的构造函数的原型对象(prototype )。该原型对象也有一个自己的原型对象( proto ) ,层层向上直到一个对象的原型对象为
null
。根据定义,null
没有原型,并作为这个原型链中的最后一个环节。几乎所有 JavaScript 中的对象都是位于原型链顶端的
Object
的实例。尽管这种原型继承通常被认为是 JavaScript 的弱点之一,但是原型继承模型本身实际上比经典模型更强大。例如,在原型模型的基础上构建经典模型相当简单。
console.log(Function.prototype.__proto__ === Object.prototype);//true //Function的prototype是Object的实例化 //所有的对象都是Object的实例化,所以Function.prototype是一个对象,是Object的实例化
console.log(Object.__proto__ === Function.prototype);//true Object是Function的实例化
console.log(Function.__proto__ === Function.prototype);//true Function 函数对象是由它本身创建的, 所以Function的__proto__ 就是Function的prototype
console.log(Object.prototype.__proto__ === null);//true
1. 原型链继承
基本思想: 利用原型让一个引用类型继承另一个引用类型的属性和方法
核心:原型链对象 变成 父类实例,子类就可以调用父类方法和属性。
function Parent() {
}
Parent.prototype.age = 18
Parent.prototype.getName = function () {return this.name
}function Child(name) {this.name = name
}
Child.prototype = new Parent()var child = new Child('leo')
// 这样子类就可以调用父类的属性和方法
console.log(child.getName()) // leo
console.log(child.age) // 18
优点: 实现简单。
缺点:
- 引用类型值的原型属性会被所有实例共享。
- 不能向父类传递参数。
function Parent() {this.likeFood = ['水果', '鸡', '烤肉']
}
Parent.prototype.age = 18
Parent.prototype.getName = function () {return this.name
}function Child(name) {this.name = name
}
Child.prototype = new Parent()var chongqiChild = new Child('重庆孩子')
var guangdongChild = new Child('广东孩子')// 重庆孩子还喜欢吃花椒。。。
chongqiChild.likeFood.push('花椒')
console.log(chongqiChild.likeFood) // ["水果", "鸡", "烤肉", "花椒"]
console.log(guangdongChild.likeFood) // ["水果", "鸡", "烤肉", "花椒"]
这时,会发现明明只是 重庆孩子 爱吃花椒,广东孩子 莫名奇妙得也变得爱吃了????这个共享是存在问题的,不科学的。(可能重庆孩子和广东孩子一起黑脸问号。。。)
至于第二个问题,其实也显而易见了,没有传递参数的途径。因此,第二种继承方式出来啦。
2. 借用构造函数继承
遗留问题:
- 父类引用属性共享。
- 不能传参数到父类。
核心:子类构造函数内部调用父类构造函数,并传入 this指针。
// 2. 借用构造函数
function Parent(name) {this.name = namethis.likeFood = ["水果", "鸡", "烤肉"]
}
function Child(name) {Parent.call(this, name)
}
Parent.prototype.getName = function() {return this.name
}
var chongqingChild = new Child('重庆孩子')
var guangdongChild = new Child('广东孩子')
chongqingChild.likeFood.push('花椒')console.log(chongqingChild.likeFood) // ["水果", "鸡", "烤肉", "花椒"]
console.log(guangdongChild.likeFood) // ["水果", "鸡", "烤肉"]
console.log(chongqingChild.name) // "重庆孩子"
console.log(chongqingChild.getName()) // Uncaught TypeError: chongqingChild.getName is not a function
值得庆幸的是,这次只有我们 重庆孩子 喜欢吃花椒,广东孩子 没被标记爱吃花椒啦。并且,我们通过 call 方法将我们的参数也传入到了父类,解决了之前的遗留问题啦。
但是,原型链继承 是可以调用父类方法的,但是借用构造函数却不可以了,这是因为 当前子类的原型链并不指向父类了。因此,结合 第一,第二种继承方式,第三种继承方式应运而生啦。
3. 组合继承
核心: 前两者结合,进化更高级。
function Parent(name) {this.name = namethis.likeFood = ["水果", "鸡", "烤肉"]
}
function Child(name, age) {Parent.call(this, name)this.age = age
}
Parent.prototype.getName = function() {return this.name
}
Child.prototype = new Parent()
Child.prototype.constructor = Child
Child.prototype.getAge = function() {return this.age
}var chongqingChild = new Child('重庆孩子', 18)
var guangdongChild = new Child('广东孩子', 19)
chongqingChild.likeFood.push('花椒')console.log(chongqingChild.likeFood) // ["水果", "鸡", "烤肉", "花椒"]
console.log(guangdongChild.likeFood) // ["水果", "鸡", "烤肉"]
console.log(chongqingChild.name) // "重庆孩子"
console.log(chongqingChild.getName()) // "重庆孩子"
console.log(chongqingChild.getAge()) // 18
这样:
- 原型引用类型传参共享问题
- 传参问题
- 调用父类问题都解决啦。
- Javascript 的经典继承。
- 但是有一个小缺点:在给 Child 原型赋值会执行一次Parent构造函数。所以,无论什么情况下都会调用两次父类构造函数
4. 原型式继承
这是在2006年一个叫 道格拉斯·克罗克福德 的人,介绍的一种方法,这种方法并没有使用严格意义上的构造函数。
他的想法是 借助原型可以基于已有的对象创建新对象,同时还不必因此创建自定义类型。
这之前的三种继承方式,我们都需要自己写自定义函数(例如,Parent和Child)。假如,现在已经有一个对象了,并且,我也只是想用你的属性,不想搞得那么麻烦的自定义很多函数。那怎么办呢?
核心: 我们需要创建一个临时的构造函数,并将作为父类的对象作为构造函数的原型,并返回一个新对象。
/*@function 实现继承 函数@param parent 充当父类的对象
*/
function realizeInheritance(parent) {// 临时函数function tempFunc() {}tempFunc.prototype = parentreturn new tempFunc()
}
核心点说了,我们来尝试一下。
// 这个就是已有的对象
var baba = {name: "爸爸",likeFoods: ["水果", "鸡", "烤肉"]
}
/*var newChild = {} <==> baba 这两个对象建立关系就是这种继承的核心了。
*/
var child1 = realizeInheritance(baba)
var child2 = realizeInheritance(baba)
child1.likeFoods.push('花椒')
console.log(child1.likeFoods) // ["水果", "鸡", "烤肉", "花椒"]
console.log(child2.likeFoods) // ["水果", "鸡", "烤肉", "花椒"]
我们可以发现,父类的属性对于子类来说都是共享的。所以,如果我们只是想一个对象和另一个对象保持一致,这将是不二之选。
ES5 新增了个 Object.create(parentObject) 函数来更加便捷的实现上述继承
var baba = {name: "爸爸",likeFoods: ["水果", "鸡", "烤肉"]
}
var child1 = Object.create(baba)
var child2 = Object.create(baba)
child1.likeFoods.push('花椒')
console.log(child1.likeFoods) // ["水果", "鸡", "烤肉", "花椒"]
console.log(child2.likeFoods) // ["水果", "鸡", "烤肉", "花椒"]
效果和上面相同~
5. 寄生式继承
这种继承是基于原型式继承,是同一个人想出来的,作者觉得,这样不能有子类的特有方法,似乎不妥。就用来一个种工厂模式的方式来给予子类一些独特的属性。
function realizeInheritance(parent) {// 临时函数function tempFunc() {}tempFunc.prototype = parentreturn new tempFunc()
}
// Parasitic: 寄生的 inheritance: 继承 一个最简单的工厂函数。
function parasiticInheritance(object) {var clone = realizeInheritance(object) // 这是用了原型式继承,但是只要是任何可以返回对象的方法都可以。clone.sayName = function() {console.log('我是'+this.name)}return clone
}
var baba = {name: "爸爸",likeFoods: ["水果", "鸡", "烤肉"]
}
var child = parasiticInheritance(baba)
child.name = '儿子'
child.sayName() // 我是儿子
缺点:使用寄生式继承来为对象添加函数,会由于不能做到函数复用而降低效率(每一个函数都是新的);这一点与构造函数继承类似。
6.寄生组合式继承
我们先回顾之前的 组合继承
function Parent(name) {this.name = namethis.likeFood = ["水果", "鸡", "烤肉"]
}
function Child(name, age) {Parent.call(this, name) // 第二次调用this.age = age
}
Parent.prototype.getName = function() {return this.name
}
Child.prototype = new Parent() // 第一次调用
Child.prototype.constructor = Child
Child.prototype.getAge = function() {return this.age
}
这个两次调用的问题之前有提及过。过程大致:
- 第一次调用,Child 的原型被赋值了 name 和 likeFood 属性
- 第二次调用,注入this,会在Child 的实例对象上注入 name 和 likeFood 属性,这样就屏蔽了原型上的属性。
只要了问题,我们就来解决这个问题~
function Parent(name) {this.name = namethis.likeFood = ["水果", "鸡", "烤肉"]
}
function Child(name, age) {Parent.call(this, name)this.age = age
}
Parent.prototype.getName = function() {return this.name
}// Child.prototype = new Parent() 使用新方法解决
// Child.prototype.constructor = Child
inheritPrototype(Child, Parent)
function inheritPrototype(childFunc, parentFunc) {var prototype = realizeInheritance(parentFunc.prototype) //创建对象,我们继续是用原型式继承的创建prototype.constructor = childFunc //增强对象childFunc.prototype = prototype //指定对象
}
function realizeInheritance(parent) {// 临时函数function tempFunc() {}tempFunc.prototype = parentreturn new tempFunc()
}Child.prototype.getAge = function() {return this.age
}var chongqingChild = new Child('重庆孩子', 18)
var guangdongChild = new Child('广东孩子', 19)
chongqingChild.likeFood.push('花椒')console.log(chongqingChild.likeFood) // ["水果", "鸡", "烤肉", "花椒"]
console.log(guangdongChild.likeFood) // ["水果", "鸡", "烤肉"]
console.log(chongqingChild.name) // "重庆孩子"
console.log(chongqingChild.getName()) // "重庆孩子"
console.log(chongqingChild.getAge()) // 18
这种方法的核心思想:
- 首先,用一个空对象建立和父类关系。
- 然后,再用这个空对象作为子类的原型对象。
这样,中间的对象就不存在new 构造函数的情况(这个对象本来就没有自定义的函数),这样就避免了执行构造函数,这就是高效率的体现。并且,在中间对象继承过程中,父类构造器也没有执行。所以,没有在子类原型上绑定属性。
这种继承方式也被开发人员普遍认为是引用类型最理想的继承范式。
总结
- 模式(简述):
- 工厂模式:创建中间对象,给中间对象赋添加属性和方法,再返回出去。
- 构造函数模式:就是自定义函数,并用过 new 关键子创建实例对象。缺点也就是无法复用。
- 原型模式: 使用 prototype 来规定哪一些属性和方法能被共享。
- 继承
- 原型链继承:
- 优点:只调用一次父类构造函数,能复用原型链属性
- 缺点:部分不想共享属性也被共享,无法传参。
- 构造函数继承:
- 优点:可以传参,同属性可以不被共享。
- 缺点:无法使用原型链上的属性
- 组合继承
- 优点:可以传参,同属性可以不被共享,能使用原型链上的属性。
- 缺点:父类构造函数被调用2次,子类原型有冗余属性。
- 原型式继承:(用于对象与对象之间)
- 优点:在对象与对象之间无需给每个对象单独创建自定义函数即可实现对象与对象的继承,无需调用构造函数。
- 缺点:父类属性被完全共享。
- 寄生式继承:
- 优点:基于原型式继承仅仅可以为子类单独提供一些功能(属性),无需调用构造函数。
- 缺点:父类属性被完全共享。
- 寄生组合继承:
- 优点:组合继承+寄生式继承,组合继承缺点在于调用两次父类构造函数,子类原型有冗余属性,寄生式继承的特性规避了这类情况,集寄生式继承和组合继承的优点与一身,是实现基于类型继承的最有效方式。
- 原型链继承:
Object.create()
**
Object.create()
**方法创建一个新对象,使用现有的对象来提供新创建的对象的__proto__。
const person = {isHuman: false,printIntroduction: function() {console.log(`我的名字是 ${this.name}. 我是人吗? ${this.isHuman}`);}
};const me = Object.create(person);me.name = 'Matthew';
me.isHuman = true; me.printIntroduction();
// "我的名字是 Matthew. 我是人吗? true"
用于原型继承
function Animal(name, age) {this.name = name;this.age = age;}Animal.prototype.showName = function () {console.log(this.name, `我是${this.constructor.name}类`);}Animal.prototype.showAge = function () {console.log(this.age, `我是${this.constructor.name}类`);}function Pig(name, age, sex = "公") {Animal.call(this, name, age);this.sex = sex;}Pig.prototype = Object.create(Animal.prototype);Pig.prototype.constructor = Pig;Pig.prototype.showSex = function () {console.log(this.sex, `我是${this.constructor.name}类`);}let pig = new Pig('佩奇', 1, '母');console.log(pig);//Pig {name: "佩奇", age: 1, sex: "母"}pig.showName(); //佩奇pig.showAge(); //1pig.showSex(); //母
多重继承
function Parent1(name) {this.name = name;}Parent1.prototype.showName = function () {console.log(this.name)}Parent1.prototype.showAge = function () {console.log(this.age)}function Parent2(age) {this.age = age;}Parent2.prototype.showSomething = function () {console.log('something')}function Child(name, age, address) {Parent1.call(this, name);Parent2.call(this, age);this.address = address;}function mixProto(targetClass, parentClass, otherParent) {targetClass.prototype = Object.create(parentClass.prototype);Object.assign(targetClass.prototype, otherParent.prototype);}mixProto(Child, Parent1, Parent2)var child = new Child('佩奇', 3, '火星');console.log(child); //Child {name: "佩奇", age: 3, address: "火星"}child.showName();//佩奇child.showAge();//3child.showSomething(); //something
继承常用方式
function Pig(name, age) {this.name = name;this.age = age;}Pig.rotate = 10; //直接挂载在Pig上的静态方法和属性是不会被继承的,静态方法只能通过类来调用,实例无法调用Pig.staticShowMe = function () {//注意,如果静态方法包含`this`关键字,这个`this`指的是类,而不是实例 console.log('我是Pig的静态方法 ')}Pig.prototype.showName = function () { console.log(this.name)}//大多数继承两件套写法function Spig(name, age, sex) {Pig.call(this, name, age);//属性的继承this.sex = sex;//特有属性}Spig.prototype = Object.create(Pig.prototype);//只继承原型上的方法let pig = new Spig('xxxxx', 1, 0);pig.showName();
原型案例
创建可移动对象
<!DOCTYPE html>
<html lang="zh-cn">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>私有属性 </title><style>* {margin: 0;padding: 0;}.wrap {width: 50vw;height: 80vh;background-color: #ccc;}img {position: absolute;top: 0;left: 0;}</style>
</head>
<body><div class="wrap"></div><script src="js/pig.js"></script><script>let pig = new Pig({ name: '佩奇', age: 2, weight: 888, pic: "images/p1.jpg", container: ".wrap" });let pig2 = new Pig({ name: '佩奇2', age: 2, weight: 888, pic: "images/p1.jpg", container: ".wrap" });</script>
</body>
</html>
(function (w) {const PI = 3.14;function Pig (param = {}) {this.init(param);}Pig.dragMap = {'mousedown': function (e, pig) {let ele = pig.ele.dom;let pos = pig.ele.pospos.x = e.clientX;pos.y = e.clientY;pos.left = ele.offsetLeft;pos.top = ele.offsetTop;pig.ele.isDown = true;},'mousemove': function (e, pig) {if (pig.ele.isDown) {let ele = pig.ele.dom;let pos = pig.ele.pos;let _x = e.clientX - pos.x;let _y = e.clientY - pos.y;ele.style.left = _x + pos.left + 'px';ele.style.top = _y + pos.top + 'px';}},'mouseup': function (e, pig) {pig.ele.isDown = false;}};Pig.prototype.siblings = [];//Array 引用类型Pig.prototype.init = function ({ name = 'p1', age = 1, weight = 100, pic = "images/pig.jpg", container = '.container' }) {this.name = name;this.age = age;this.weight = weight;this.picUrl = pic;this.container = document.querySelector(container);this.size = this.container.offsetWidth / 10;this.siblings.push(this);this.draw();this.dragInit();}Pig.prototype.draw = function () {const vDom = document.createElement('img');vDom.src = this.picUrl;vDom.width = this.size;this.ele = {dom: vDom,pos: {x: 0,y: 0,left: 0,top: 0},isDown: false}this.container.appendChild(vDom);}Pig.prototype.dragInit = function () {let eleImg = this.ele.dom;let drag = (e) => {e.preventDefault();//this 指向 监听事件的DOM对象if (Pig.dragMap[e.type]) {Pig.dragMap[e.type](e, this);}return false;}eleImg.addEventListener('mousedown', drag, false);w.document.addEventListener('mousemove', drag, false);w.document.addEventListener('mouseup', drag, false);}Pig.prototype.eat = function () {console.log(`我是${this.name} 我${this.age}岁了 我要吃饭了`);}Pig.prototype.bloodReturn = function (len) {//根据一共实例化了多少头猪来确定回多少血console.log(`回血 ${len * 100}`);}//料肉比 饲料/增肉量 Pig.prototype.feedConversionRatio = 2.4;w.Pig = Pig;//Pig构造函数挂载在 全局window对象上
})(window);
Object 深入
Object方法
Object.getOwnPropertyNames()
//方法返回一个由指定对象的所有自身属性的属性名(包括不可枚举属性但不包括Symbol值作为名称的属性)组成的数组。
let arr = [1, 2, 3];
Object.getOwnPropertyNames(arr)//['0', '1', '2', 'length']
Object.keys(arr)// Object.keys是返回可枚举属性['0', '1', '2']Object.getPrototypeOf()
//方法返回指定对象的原型(内部[[Prototype]]属性的值)。
//如果没办法使用__proto__就使用这个
Object.getPrototypeOf(arr) === arr.__proto__ //trueObject.getOwnPropertyDescriptors()
//方法用来获取一个对象的所有自身属性的描述符。
const O = {name: 'kyogre',age: 123
};
let descriptor = Object.getOwnPropertyDescriptors(O);
console.log(descriptor);
//{age:
//{configurable: true//可配置
//enumerable: true//可枚举
//value: 123
//writable: true}//可写
//name:
//{configurable: true
//enumerable: true
//value: "kyogre"
//writable: true}}Object.getOwnPropertyDescriptor()
//方法返回指定对象上一个自有属性对应的属性描述符。(自有属性指的是直接赋予该对象的属性,不需要从原型链上进行查找的属性)
//console.log(Object.getOwnPropertyDescriptor(O,'name'))
//{value: 'kyogre', writable: true, enumerable: true, configurable: true}Object.assign()
//方法用于将所有可枚举属性的值从一个或多个源对象复制到目标对象。它将返回目标对象,不会合并__proto__不可枚举对象
//合并的对象同名属性会被覆盖,数组也可以合并,数组的下标作为对象的键Object.create()
//方法创建一个新对象,使用现有的对象来提供新创建的对象的__proto__。 (请打开浏览器控制台以查看运行结果。)Object.freeze()
//方法可以冻结一个对象。一个被冻结的对象再也不能被修改;冻结了一个对象则不能向这个对象添加新的属性,不能删除已有属性,不能修改该对象已有属性的可枚举性、可配置性、可写性,以及不能修改已有属性的值。此外,冻结一个对象后该对象的原型也不能被修改。freeze() 返回和传入的参数相同的对象。
//被冻结后打印的对象的属性描述符中configurable: false不可配置,也不可写入writable: falseObject.isFrozen()
//方法判断一个对象是否被冻结。Object.isSealed()
//方法判断一个对象是否被密封。hasOwnProperty()
//方法会返回一个布尔值,判断一个对象的属性是否为自身的属性而非原型上的属性
O.hasOwnProperty('name')//true
O.hasOwnProperty('valueOf')//false 原型属性
O.hasOwnProperty('a')//false 属性不存在也是falseisPrototypeOf()
//方法用于测试一个对象是否存在于另一个对象的原型链上。Object.is()
//方法判断两个值是否为同一个值。相当于===。指针不一致也是false
let arr1 = [1, 2, 3];
let arr2 = [1, 2, 3];
console.log(Object.is(arr1, arr2));//false
let arr1 = [1, 2, 3];
let arr2 = arr1;
console.log(Object.is(arr1, arr2));//true
Object.defineProperty()
方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。
语法
Object.defineProperty(obj, prop, descriptor)
参数
-
obj
要定义属性的对象。
-
prop
要定义或修改的属性的名称或
Symbol
。 -
descriptor
要定义或修改的属性描述符。
返回值
被传递给函数的对象。
该方法允许精确地添加或修改对象的属性。通过赋值操作添加的普通属性是可枚举的,在枚举对象属性时会被枚举到(for...in 或 Object.keys 方法),可以改变这些属性的值,也可以删除这些属性。这个方法允许修改默认的额外选项(或配置)。默认情况下,使用 Object.defineProperty() 添加的属性值是不可修改(immutable)的。对象里目前存在的属性描述符有两种主要形式:数据描述符和存取描述符。数据描述符是一个具有值的属性,该值可以是可写的,也可以是不可写的。存取描述符是由 getter 函数和 setter 函数所描述的属性。一个描述符只能是这两者其中之一;不能同时是两者。这两种描述符都是对象。它们共享以下可选键值(默认值是指在使用 Object.defineProperty() 定义属性时的默认值):
-
configurable
当且仅当该属性的
configurable
键值为true
时,该属性的描述符才能够被改变,同时该属性也能从对应的对象上被删除。 默认为false
,为false时,即使用delete也无法删除对象的属性。 -
enumerable
当且仅当该属性的
enumerable
键值为true
时,该属性才会出现在对象的枚举属性中。 默认为false
。
数据描述符还具有以下可选键值:
-
value
该属性对应的值。可以是任何有效的 JavaScript 值(数值,对象,函数等)。 默认为
undefined
。 -
writable
当且仅当该属性的
writable
键值为true
时,属性的值,也就是上面的value
,才能被赋值运算符
改变。 默认为false
。
存取描述符还具有以下可选键值:
-
get
属性的 getter 函数,如果没有 getter,则为
undefined
。当访问该属性时,会调用此函数。执行时不传入任何参数,但是会传入this
对象(由于继承关系,这里的this
并不一定是定义该属性的对象)。该函数的返回值会被用作属性的值。 默认为undefined
。 -
set
属性的 setter 函数,如果没有 setter,则为
undefined
。当属性值被修改时,会调用此函数。该方法接受一个参数(也就是被赋予的新值),会传入赋值时的this
对象。 默认为undefined
。
描述符默认值汇总
- 拥有布尔值的键
configurable
、enumerable
和writable
的默认值都是false
。 - 属性值和函数的键
value
、get
和set
字段的默认值为undefined
。
描述符可拥有的键值
configurable | enumerable | value | writable | get | set | |
---|---|---|---|---|---|---|
数据描述符 | 可以 | 可以 | 可以 | 可以 | 不可以 | 不可以 |
存取描述符 | 可以 | 可以 | 不可以 | 不可以 | 可以 | 可以 |
如果一个描述符不具有 value
、writable
、get
和 set
中的任意一个键,那么它将被认为是一个数据描述符。如果一个描述符同时拥有 value
或 writable
和 get
或 set
键,则会产生一个异常。
记住,这些选项不一定是自身属性,也要考虑继承来的属性。为了确认保留这些默认值,在设置之前,可能要冻结 Object.prototype
,明确指定所有的选项,或者通过 Object.create(null)
将 __proto__
属性指向 null
。
Object.defineProperty()
可以对单条属性进行修改
let o = {name: 'kyogre',age: 13}Object.defineProperty(o, 'age', {configurable: true, //可配置enumerable: true, //可枚举value: 19, //属性值writable: true,//可写入});
Object.defineProperties()
可以对多条属性进行修改
let o = {name: 'kyogre',age: 13}Object.defineProperties(o, {"name": {configurable: true, //可配置enumerable: true, //可枚举value: 19, //属性值writable: true,//可写入},"age": {configurable: true, //可配置enumerable: true, //可枚举value: 19, //属性值writable: true,//可写入}})
Object.defineProperty()
的封装
//使用get()、set()设置及获取数据的封装let O = {name: 'o'}defineReactive(O, 'name', 'o');function defineReactive(obj, key, val{Object.defineProperty(obj, key, {set(newValue) {value = newValue;},get() {return value;}});}O.name = '大笨蛋';console.log(O.name)//'大笨蛋'
Object.entries()
方法返回一个给定对象自身可枚举属性的键值对数组,其排列与使用
for...in
循环遍历该对象时返回的顺序一致(区别在于 for-in 循环还会枚举原型链中的属性)。
语法
Object.entries(obj)
参数
-
obj
可以返回其可枚举属性的键值对的对象。
返回值
给定对象自身可枚举属性的键值对数组。
const object1 = {a: 'somestring',b: 42
};console.log(Object.entries(object1));//[['a', 'somestring'],['b', 42]]
for (const [key, value] of Object.entries(object1)) {console.log(`${key}: ${value}`);
}
// "a: somestring"
// "b: 42"
案例:使用Object.entries()可以很好的将对象的属性或属性值返回成一个数组
let o = {t: 1,b: 2,l: 3,r: 4
}console.log(Object.entries(o).map(([key, value]) => (key)))//取键['t', 'b', 'l', 'r']
console.log(Object.entries(o).map(([key, value]) => (value)))// 取值[1, 2, 3, 4]
console.log(Object.entries(o).map(([key, value]) => ([key, value * 3])))//处理值后返回新数组[['t', 3],['b', 6],['l', 9],['r', 12]]//可以使用
Object.keys()
方法会返回一个由一个给定对象的自身可枚举属性组成的数组,数组中属性名的排列顺序和正常循环遍历该对象时返回的顺序一致 。。
语法
Object.keys(obj)
参数
-
obj
要返回其枚举自身属性的对象。
返回值
一个表示给定对象的所有可枚举属性的字符串数组。
const arr = ['a', 'b', 'c'];
console.log(Object.keys(arr)); // console: ['0', '1', '2']// array like object
const obj = { 0: 'a', 1: 'b', 2: 'c' };
console.log(Object.keys(obj)); // console: ['0', '1', '2']
Object.fromEntries()
方法把键值对列表转换为一个对象。
语法
Object.fromEntries(iterable);
参数
-
iterable
类似
Array
、Map
或者其它实现了可迭代协议的可迭代对象。
返回值
一个由该迭代对象条目提供对应属性的新对象
const map = new Map([ ['foo', 'bar'], ['baz', 42] ]);
const obj = Object.fromEntries(map);
console.log(obj); // { foo: "bar", baz: 42 }
Object.preventExtensions()
方法让一个对象变的不可扩展,也就是永远不能再添加新的属性,只能对已有属性进行操作,如删除或修改已有属性。
语法
Object.preventExtensions(obj)
参数
-
obj
将要变得不可扩展的对象。
返回值
已经不可扩展的对象。
var obj = {};
var obj2 = Object.preventExtensions(obj);
obj === obj2; // true// 字面量方式定义的对象默认是可扩展的.
var empty = {};
Object.isExtensible(empty) //=== true// ...但可以改变.
Object.preventExtensions(empty);
Object.isExtensible(empty) //=== false// 使用Object.defineProperty方法为一个不可扩展的对象添加新属性会抛出异常.
var nonExtensible = { removable: true };
Object.preventExtensions(nonExtensible);
Object.defineProperty(nonExtensible, "new", { value: 8675309 }); // 抛出TypeError异常// 在严格模式中,为一个不可扩展对象的新属性赋值会抛出TypeError异常.
function fail()
{"use strict";nonExtensible.newProperty = "FAIL"; // throws a TypeError
}
fail();const map = new Map([ ['foo', 'bar'], ['baz', 42] ]);
const obj = Object.fromEntries(map);
console.log(obj); // { foo: "bar", baz: 42 }
深拷贝
深拷贝:就是完完全全拷贝一份新的对象,它会在内存的堆区域重新开辟空间,修改拷贝对象就不会影响到源对象。
浅拷贝:拷贝基本数据类型时,不受任何影响,当拷贝引用类型时,源对象也会被修改。
JSON.parse(JSON.stringify(待拷贝对象))
缺点:没办法拷贝内部函数,也没办法拷贝正则对象
let a = {name : '张三',age : '18',a:/^a$/,like(){console.log('喜欢唱歌、滑冰');}
}
let b =JSON.parse( JSON.stringify(a) );
b.name = '李四';
console.log('a:',a);
//a:{name: '张三', age: '18', a: /^a$/, like: ƒ like()}
console.log('b:',b);
//b:{name: '李四', age: '18', a: {}}let a = {name : '张三',age : '18',a:/^a$/,like(){console.log('喜欢唱歌、滑冰');}
}
let b = {...a};
b.name = '李四';
console.log('a:',a);//{name: '张三', age: '18', a: /^a$/, like: ƒ}
console.log('b:',b);//{name: '李四', age: '18', a: /^a$/, like: ƒ}
es6的展开语法拷贝
缺点:即只能深拷贝第一层,对于多层拷贝无效
js中针对数组Array的slice和concat方法,也是只能拷贝第一层
const a = {arr: [1, 2, " % E4 % BD % A0 % E5 % A5 % BD", { x: 1, y: [1, 2, 3] }],json: {key: [1, 2, 3, 4],value: {a: null,b: undefined,c: function () {console.log('c');}}},name: 'oooo'
}
let b = {...a};b.arr[3].x = '222222';
console.log('a:',a);
console.log('b:',b);//a和b的arr[3].x一起改变为222222
递归实现深拷贝
const a = {arr: [1, 2, " % E4 % BD % A0 % E5 % A5 % BD", { x: 1, y: [1, 2, 3] }],json: {key: [1, 2, 3, 4],value: {a: null,b: undefined,c: function () {console.log('c');}}},name: 'oooo'}function deepCopy(original) {if (Array.isArray(original)) {return original.map(ele => deepCopy(ele));}if (typeof original === 'object' && original !== null) {return Object.fromEntries(Object.entries(original).map(([key, value]) => [key, deepCopy(value)]));}else {return original;}}const b = deepCopy(a)b.arr[3].x = '222222';console.log('a:',a);console.log('b:',b);
递归函数
前进阶段(递归阶段)
终止阶段(返回阶段)
必须有结束条件(递归边界)
递归练习
- 找宝藏
找到对象 o里面的属性[[[c]]]的值为 ‘恭喜你’,找到后返回 ‘恭喜你找到宝藏’
let o = {c: {c: {c: {c: {c: '恭喜你'}}}}}let count = 0;function findResult(o) {if (o['c'] !== '恭喜你') {count++;return findResult(o['c']); //前进}return '恭喜你找到宝藏'; //终止}console.log(findResult(o));console.log(count);
- 1-100的和
let result = (1 + 100) * (100 / 2);console.log(result);let c = 0;for (let i = 1; i <= 100; i++) {c += i;}console.log(c);console.log(getCountNum(100));function getCountNum(n) {if (n === 1) return 1;return n + getCountNum(n - 1);}
- 编写一个函数实现n^k (n的k次方),使用递归实现
function getPow(n, k) {k--;if (k === 0) {//出口条件return n;}//前进条件return n * getPow(n, k);}console.log(getPow(3, 5));
-
角谷定理。输入一个自然数,若为偶数,则把它除以2,若为奇数,则把它乘以3加1。经过如此有限次运算后,总可以得到自然数值1。求经过多少次可得到自然数1。
function fn1(n, c = 0) {c++;if (n === 2) {//倒数第二次return c;}return fn1(((n % 2 === 0) ? n / 2 : n * 3 + 1), c);}console.log(fn1(5));
-
一个人赶着鸭子去每个村庄卖,每经过一个村子卖去所赶鸭子的一半又一只。这样他经过了7个村子后还剩2鸭子,问他出发时共赶多少只鸭子?经过每个村子卖出多少只鸭子?
function fn2(n, t, arr = []) { //2 ,7if (t === 0) {//终止return {first: n,arr: arr};}arr.unshift(n + 2);t--; // 6//前进return fn2(2 * (n + 1), t, arr);}console.log(fn2(2, 7));;
- 数列 0,1,1,2,4,7,13,24,44,…求数列的第 n项 (找规律) 4= 1 + 1 +2 7 = 1+2+4
function fn3(n) {if (n === 0 || n === 1) {return 1;}if (n === 2) {return 2;}return fn3(n - 3) + fn3(n - 2) + fn3(n - 1);}console.log(fn3(2))