搞懂proto与prototype

一,前言对一个知识点是否完全把握,最好的校验方法就是能否用自己的语言将其表述出来。原型与原型链一直是学习 JS 绕不过的知识点,其中proto 与 prototype 最为让人头疼,这里简单的写下我自己的理解,从原型与原型链中拆解 proto 与 prototype ,希望能对大家有所帮助。 一,原型1,定义 在javascript中,函数可以有属性。 每个函数都有一个特殊的属性叫作原型(prototype)注:prototype属性是函数特有,对象没有该属性原型(prototype) 是什么东西呢,里面又有哪些属性呢?来,让我们拿个具体例子看下: function fn() {};console.dir(fn) 这里我们可以看出 原型(prototype) 是一个对象,对于当前例子来说,里面有 constructor 与 proto两个属性。那这个原型对象有什么作用呢?来,让我们继续往下看~ 2,使用 现在我们知道了原型是一个对象,那原型对象有什么作用呢?实际上,原型是 ECMAScript 实现继承的过程中产生的一个概念。这里我们简单拿 ES5 的对象来举例子: function Person(name) { this.name = name;}Person.prototype.say = function() { console.log(`My name is ${this.name}`);}let person = new Person('小明');person.say(); // My name is 小明让我们来逐步解释下这个例子: 声明了一个构造函数 Person,其有一个属性 name在其原型上声明了一个函数 say实例一个 Person 类——person调用 person 的 say 方法我们发现,person可以调用其构造函数原型里的say方法,why?让我们看下 person 里都有什么: 实例 person 虽然自身没有 say 方法,但是通过 proto 属性访问到了其原型中的 say 方法。 ...

October 9, 2019 · 2 min · jiezi

JavaScript进阶-1-原型和原型链的概念

JavaScript进阶 - 1. 原型和原型链的概念我们好多经常会被问道JavaScript原型和原型链的概念,还有关于继承,new操作符相关的概念。本文就专门整理了原型和原型链的概念,关于对象继承我们后边进行介绍。本文包含对应的示例代码和脑图。如有奇异,欢迎指正! 目录讲解1. 构造函数创建对象2. 相关的名词介绍 2.1 prototype2.2 proto2.3 constructor3. 实例和原型 原型和原型4. 原型链是怎么产生的(附有相关的关系图说明)1. 构造函数创建对象我们先使用构造函数的方式声明一个对象: function Person() {}let person = new Person()person.name = '小红'console.log(person.name) // 小红在上面的代码中。Person是构造函数, person是通过new方式创建的实例对象。现在开始进入一个环节 2. 相关的名词介绍prototype,constructor,__proto__是我们经常见到的几个概念,但是他们之间的关系具体是什么样的呢,让我们逐步开始了解。 2.1 prototype每个函数都有 prototype 属性,除了 Function.prototype.bind(),该属性指向原型, prototype: 指向实例对象的原型对象 function Person() {}Person.prototype.name = '小红'let person = new Person()console.log(person.name) // 小红Person这个函数有声明prototype属性,那么这个值指向的到底是哪儿,是原型对象吗? 其实prototype指向的是,调用当前构造函数创建实例对象的原型,也就是person的原型。 那么原型到底是什么呢?其实原型可以理解为,一个JavaScript对象(null除外)在创建的时候会关联另外一个对象,这个被关联的对象就是我们说的原型对象,实例对象会在创建的时候,从原型对象继承一些属性或者方法。 对应的关系图如下: 2.2 protoprototype讲解了构造函数和原型对象之间的关系,那么实例对象和原型对象之间的关系又是怎么样的呢?下面讲解。每个JavaScript对象(null除外),都会有个__proto__的属性,这个属性指向的就是原型对象 function Person() {}let person = new Person()console.log(person.__proto__ === Person.prototype) // true// ES5 通过实例对象获取原型对象的方法console.log(Object.getPrototypeOf(person) === Person.prototype) // true由此我们上面的管理系图谱可以补充为: ...

July 7, 2019 · 1 min · jiezi

JavaScript-之-原型和原型链

几个概念构造函数: 构造函数实际上是一个普通函数,通过new操作符,可以利用构造函数快速创建对象;prototype:每个构造函数自身上都会有一个prototype属性,该属性值为一个对象,这个对象具有一个constructor属性,constructor指向构造函数自身;实例: 通过构造函数创建的对象,可以看做是该构造函数的实例;__proto__:通过构造函数创建的每个对象上面,都会有一个__proto__属性,该属性指向构造函数的prototype;什么是原型在JavaScript中,每个对象会有一个原型对象。对象会从原型对象继承一些属性和方法。 什么是原型链在JavaScript中,访问一个对象的属性时,JS引擎会首先在该对象自身上线查找该属性。如果找到了,则直接返回该属性的值;如果没有找到,则会去改对象的原型上面继续查找。如果对象的原型上面也没有这个属性,则继续在原型对象的上原型上面查找,如此一级级继续往上,直到原型为null,此时JS引擎返回该对象的属性值为undefined。 继承 /** * two methods to implement inheritance; */ function Base(type){ this.type = type; } Base.prototype.base=function(){ console.log(`${this.type} is in base func`); } // method one function Sub(type){ this.type = type; } Sub.prototype = Object.create(new Base('base')); Sub.prototype.sub=function(){ console.log(`${this.type} is in sub func`); } // method two function Foo(type){ this.type = type; } Object.setPrototypeOf( Foo.prototype, new Sub('sub')); Foo.prototype.foo=function(){ console.log(`${this.type} is in foo func`); } let sub = new Sub('sub1'); sub.base(); sub.sub(); sub instanceof Sub; // true sub instanceof Base; // true let foo = new Foo('foo1'); foo.base(); foo.sub(); foo.foo(); foo instanceof Foo; // true foo instanceof Sub; // true foo instanceof Base; // true一些实例Object.getPrototype预测下面几个表达式的结果 ...

July 1, 2019 · 1 min · jiezi

JS中的prototypeproto与constructor

1. 寻找原型心法口诀:每个对象的原型(__proto__)都指向自身的构造函数(constructor)的prototype属性let b={}b.constructor === Object// trueb.__proto__ === Object.prototype// trueb.__proto__ === b.constructor.prototype// true所以想要知道某个对象的原型是什么,首先找到他的构造函数是什么9个终极类Array.constructor// ƒ Function() { [native code] }Boolean.constructor// ƒ Function() { [native code] }Date.constructor// ƒ Function() { [native code] }Number.constructor// ƒ Function() { [native code] }String.constructor// ƒ Function() { [native code] }Object.constructor// ƒ Function() { [native code] }RegExp.constructor// ƒ Function() { [native code] }Symbol.constructor// ƒ Function() { [native code] }1个究极类Function.constructor// ƒ Function() { [native code] }3中特殊数字对象Math.constructor// ƒ Object() { [native code] }// Math 对象并不像 Date 和 String 那样是对象的类,因此没有构造函数 Math()NaN.constructor// ƒ Number() { [native code] }Infinity.constructor// ƒ Number() { [native code] }2中bug类型undefined.constructor// VM25366:1 Uncaught TypeError: Cannot read property 'constructor' of undefined at <anonymous>:1:11null.constructor// VM25366:1 Uncaught TypeError: Cannot read property 'constructor' of null at <anonymous>:1:11... ...

June 26, 2019 · 1 min · jiezi

图解javascript原型原型链

我们在学习javascript时,经常会听到“万物皆对象”,但是呢,其实万物皆对象的对象也有区别。分为普通对象和函数对象。1.对象分为函数对象和普通对象    通过new Function()创建的对象都是函数对象,其他的都是普通对象。 2.构造函数而提到new关键字,我们不得不提到构造函数。构造函数又分为自定义构造函数及native构造函数(即构造器)2.1 自定义构造函数 function Person(name,age,job){        this.name = name;        this.age = age;        this.job = job;    }    var miya = new Person('miya',18,'engineer'); //miya是构造函数Person的实例;而实例的构造函数属性(constructor) 都指向构造函数    即:miya.constuctor = Person     2.2 构造器     js内置的构造器包括Number,Boolean,String,Object,Function,Array,RegExp,Error,Date等同样的,我们的构造器的实例也有一个constuctor指向它的构造器    let miya= new Array()    miya.constuctor ==Array;通过以上的分析,我们可以得到以下这张图    这里扩展一下,其实我们在做类型判断的时候习惯用typeof,typeof判断简单数据类型的时候其实是ok的。但是typeof有判断复杂数据类型不是很清晰,得到的基本是String或者Function比如一下图示判断但是,通过上述我们知道构造器的实例都有一个constructor的属性指向构造器。那其实我们直接用constructor便可以清晰地知道这个实例从哪里来。3.原型对象    每个函数对象都有一个prototype属性,这个属性指向函数的原型对象即prototype。    所有的prototype会有一个默认的constuctor属性,指向函数对象本身。     function Person(name,age,job){this.name = name; this.age = age; this.job = job;}Person.prototype.sayName = function () { console.log(this.name);};let miya = new Person('miya',18,'engineer');let tiffany = new Person('tiffany',18,'engineer');miya.sayName(); //miyatiffany.sayName(); //tiffanyconsole.log(miya.sayName == tiffany.sayName); //trueconsole.log(Person.prototype.constructor == Person); //true通过:Person.prototype.constuctor = Person ,因为构造函数的实例是有一个constructor的属性指向构造函数的,我们可以得到结论其实Person.prototype也是Person的实例    即:原型对象是构造函数的一个实例【原型对象(Function.prototype除外)是一个普通对象,而不是函数对象】 ...

June 15, 2019 · 1 min · jiezi

JS核心知识点梳理原型继承上

引言最近又攀登了一下JS三座大山中的第二座。登山过程很酸爽,一路发现了许多之前没曾注意到的美景。本着独乐乐不如众乐乐的原则,这里和大家分享一下。 JS的面试对象有些人认为 JavaScript 不是真正的面向对象的语言,比如它没有像许多面向对象的语言一样有用于创建class类的声明(在 ES2015/ES6 中引入了 class 关键字,但那只是语法糖,JavaScript 仍然是基于原型的)。JavaScript 用一种称为构建函数的特殊函数来定义对象和它们的特征。不像“经典”的面向对象的语言,从构建函数创建的新实例的特征并非全盘复制,而是通过一个叫做原形链的参考链链接过去的。同理,原型链也是实现继承的主要方式(ES6的extends只是语法糖)。 原型、原型链一直在犹豫,到底是先讲创建对象的方法还是先讲原型。为了后面保证讲创建对象方法的连贯性,这里还是先讲讲原型吧,这里为了权威,直接就摘抄MDN的定义了 JavaScript 常被描述为一种基于原型的语言 (prototype-based language)——每个对象拥有一个原型对象,对象以其原型为模板、从原型继承方法和属性。原型对象也可能拥有原型,并从中继承方法和属性,一层一层、以此类推。这种关系常被称为原型链 (prototype chain),它解释了为何一个对象会拥有定义在其他对象中的属性和方法。准确地说,这些属性和方法定义在Object的构造器函数(constructor functions)之上的prototype属性上,而非对象实例本身。 这个__proto__属性有什么用呢?在传统的 OOP 中,首先定义“类”,此后创建对象实例时,类中定义的所有属性和方法都被复制到实例中。在 JavaScript 中并不如此复制,而是在对象实例和它的构造器之间建立一个链接(它是__proto__属性,是从构造函数的prototype属性派生的),之后通过上溯原型链,在构造器中找到这些属性和方法。 简单的说,就是实例对象能通过自己的__proto__属性去访问“类”原型(prototype)上的方法和属性,类如果也是个实例,就会不断往上层类的原型去访问,直到找到 补充:1.“类”的原型有一个属性叫做constructor指向“类” 2.__proto__已被弃用,提倡使用Object.getPrototypeOf(obj) 举例: var arr = [1,2,3] //arr是一个实例对象(数组类Array的实例)arr.__proto__ === Array.prototype //true 实例上都有一个__proto__属性,指向“类”的原型Array.prototype.__proto__ === Object.prototype //true “类”的原型也是一个Object实例,那么就一定有一个__proto__属性,指向“类”object的原型这里补充一个知识点: 浏览器在在Array.prototype上内置了pop方法,在Object.prototype上内置了toString方法 上图是我画的一个原型链图 [1,2,3].pop() //3[1,2,3].toString() //'1,2,3'[1,2,3].constructor.name //"Array" [1,2,3].hehe() //[1,2,3].hehe is not a function当我们调用pop()的时候,在实例[1,2,3]上面没有找到该方法,则沿着原型链搜索"类"Array的原型,找到了pop方法并执行,同理调用toString方法的时候,在"类"Array没有找到则会继续沿原型链向上搜索"类"Object的原型,找到toString并执行。当执行hehe方法的时候,由于“类”Object的原型上并没有找到,搜索“类”Object的__proto__,由于执行null,停止搜索,报错。 注意,[1,2,3].constructor.name显示‘Array’不是说明实例上有constructor属性,而是正是因为实例上没有,所以搜索到类的原型上了,找到了constructor 类,创建对象的方法怎么创建对象,或者说怎么模拟类。这里我就不学高程一样,给大家介绍7种方法了,只讲我觉得必须掌握的。毕竟都es6 es7了,很多方法基本都用不到,有兴趣自己看高程。 利用构造函数 const Person = function (name) { this.name = name this.sayHi = function () { alert(this.name) } } const xm = new Person('小明') const zs = new Person('张三') zs.sayHi() //'张三' xm.sayHi() //'小明'缺点: 每次实例化都需要复制一遍函数到实例里面。但是不管是哪个实例,实际上sayHi都是相同的方法,没必要每次实例化的时候都复制一遍,增加额外开销。 ...

May 22, 2019 · 2 min · jiezi

JavaScript中的继承

前言作为 JavaScript 中最重要的内容之一,继承问题一直是我们关注的重点。那么你是否清晰地知道它的原理以及各种实现方式呢阅读这篇文章,你将知道:什么是继承实现继承有哪几种方式它们各有什么特点这里默认你已经清楚的知道构造函数、实例和原型对象之间的关系,如果并不是那么清晰,那么推荐你先阅读这篇文章 – JavaScript 中的原型与原型链如果文章中有出现纰漏、错误之处,还请看到的小伙伴多多指教,先行谢过以下↓概念继承(inheritance)是面向对象软件技术当中的一个概念。如果一个类别 B 继承自 另一个类别 A ,就把这个 B 称为 A的子类 ,而把 A 称为 B的父类别 也可以称 A是B的超类 。继承可以使得子类具有父类别的各种属性和方法,而不需要再次编写相同的代码 …更多)通过这些概念和图示我们不难知道继承可以在我们的开发中带来的便捷,那么在 JavaScript 中如何去实现继承呢?继承实现方式原型链继承利用原型让一个引用类型继承另一个引用类型的属性和方法function SuperType() { this.name = ’tt’;}SuperType.prototype.sayName = function() { return this.name}function SubType() { this.name = ‘oo’;}SubType.prototype = new SuperType()var instance = new SubType()instance.sayName() // ooinstance instanceof SubType // trueinstance instanceof SuperType // ture以上的试验中,我们创建了两个构造函数 SuperType 和 SubType ,并且让 SubType 的原型指向 SuperType ,SubType 也就继承了 SuperType 原型对象中的方法。所以在创建 instance 实例的时候,实例本身也就具有了 SuperType 中的方法,并且都处在它们的原型链中SubType.prototype.constructor == SubType // falseSubType.prototype.constructor == SuperType // true需要注意的是:这个时候 SubType.prototype.constructor 是指向 SuperType 的,相当于重写了 SubType 的原型对象。用一张图表示:SubType.prototype 相当于 SuperType 的实例存在的,所以 SubType.prototype.constructor 就指向 SuperType原型继承的特点优点:简单、易于实现父类新增原型方法/原型属性,子类都能访问到非常纯粹的继承关系,实例是子类的实例,也是父类的实例缺点:无法实现多继承想要为子类 SubType 添加原型方法,就必须在 new SuperType 之后添加(会覆盖)来自原型对象的所有属性被所有实例共享(引用类型的值修改会反映在所有实例上面)创建子类实例时,无法向父类构造函数传参借用构造函数在子类构造函数的内部调用超类型构造函数,通过 apply 和 call 实现function SuperType(name) { this.name = name; this.colors = [‘red’, ‘orange’, ‘black’];}function SubType() { SuperType.call(this, ’tt’);}var instance = new SubType()var instance1 = new SubType()instance.colors // [‘red’, ‘orange’, ‘black’]instance.name // ttinstance.colors.push(‘green’);instance.colors // [‘red’, ‘orange’, ‘black’, ‘green’]instance1.colors // [‘red’, ‘orange’, ‘black’]借用构造函数的特点优点:解决了原型链继承不能传参的问题子类实例共享父类引用属性的问题可以实现多继承(call可以指定不同的超类)缺点:实例并不是父类的实例,只是子类的实例只能继承父类的实例属性和方法,不能继承原型属性/方法无法实现函数复用组合继承伪经典继承(最常用的继承模式):将原型链和借用构造函数的技术组合到一起。使用原型链实现对原型属性和方法的继承,通过构造函数来实现对实例属性的继承function SuperType(name) { this.name = name; this.colors = [‘red’, ‘orange’, ‘black’];}SuperType.prototype.sayName = function() { return this.name}function SubType() { SuperType.call(this, ’tt’); this.name = ‘oo’;}// 这里的 SubType.prototype.constructor 还是指向 SuperTypeSubType.prototype = new SuperType();var instance = new SubType();var instance1 = new SubType();instance.name // ooinstance.sayName() // ooinstance.colors.push(‘green’);instance.colors // [‘red’, ‘orange’, ‘black’, ‘green’]instance1.colors // [‘red’, ‘orange’, ‘black’]组合继承的特点优点:可以继承实例属性/方法,也可以继承原型属性/方法不存在引用属性共享问题可传参函数可复用缺点:调用了两次父类构造函数,生成了两份实例(子类实例将子类原型上的那份屏蔽了)原型式继承借助原型链可以基于已有的对象创建新对象,同时还不必因此创建自定义类型function obj(o) { function F(){} F.prototype = o; return new F();}var person = { name: ’tt’, age: 18, colors: [‘red’, ‘green’]}var instance = obj(person);var instance1 = obj(person);instance.colors.push(‘black’);instance.name // ttinstance.colors // [‘red’, ‘green’, ‘black’]instance1.colors // [‘red’, ‘green’, ‘black’]创建一个临时的构造函数,然后将传入的对象当做这个构造函数的原型对象,最后返回这个临时构造函数的新实例。实际上,就是对传入的对象进行了一次浅复制ES5 通过新增 Object.create() 规范化了原型式继承更多 Object.create()语法请点击 这里原型式继承特点优点:支持多继承(传入的对象不同)不需要兴师动众的创建很多构造函数缺点: 和原型链继承基本一致,效率较低,内存占用高(因为要拷贝父类的属性)寄生式继承创建一个仅用于封装继承过程的函数,在函数内部对这个对象进行改变,最后返回这个对象function createAnother(obj) { var clone = Object(obj); clone.sayHi = function() { alert(‘Hi’); } return clone}var person = { name: ’tt’, age: 18, friends: [‘oo’, ‘aa’, ‘cc’], sayName() { return this.name }}var instance = createAnother(person)var instance1 = createAnother(person)instance.friends.push(‘yy’)instance.name // ’tt’instance.sayHi() // Hiinstance.friends // [“oo”, “aa”, “cc”, “yy”]instance1.friends // [“oo”, “aa”, “cc”, “yy”]寄生式继承的特点优点:支持多继承缺点:实例并不是父类的实例,只是子类的实例不能实现复用(与构造函数相似)实例之间会互相影响寄生组合继承借用构造函数来继承属性,通过原型链的混成形式来继承方法。通过寄生方式,砍掉父类的实例属性,这样,在调用两次父类的构造的时候,就不会初始化两次实例方法/属性,避免的组合继承的缺点function inherit(subType, superType) { var obj = Object(superType.prototype); // 创建对象 obj.constructor = subType; // 指定constructor subType.prototype = obj; // 指定对象}function SuperType(name) { this.name = name; this.colors = [‘red’, ‘orange’, ‘black’];}SuperType.prototype.sayName = function() { return this.name}function SubType() { SuperType.call(this, ’tt’); this.name = ‘oo’;}inherit(SubType, SuperType)var instance = new SubType()instance.name // ooinstance.sayName // ooinstance instanceof SubType // trueinstance instanceof SuperType // trueSubType.prototype.constructor == SubType // true寄生组合继承的特点堪称完美,只是实现稍微复杂一点后记作为 JavaScript 最重要的概念之一,对于继承实现的方式方法以及它们之间的差异我们还是很有必要了解的。在实现继承的时候,拷贝 也是一种很有效的方式,由于 JavaScript 简单数据类型与引用类型的存在,衍生出了 浅拷贝 与 深拷贝 的概念,那么它们又是什么,怎么去实现呢且听下回分解,哈哈周末愉快最后,推荐一波前端学习历程,不定期分享一些前端问题和有意思的东西欢迎 star 关注 传送门参考文档JavaScript 高级程序设计JavaScript实现继承的几种方式 ...

April 19, 2019 · 2 min · jiezi

每日一面——仿写reverse方法

引言今天小K问了我一个面试题,怎么实现一个实reverse方法,在实现的过程中我还是犯了一些错,实现完以后,对一些知识点的理解又加深了。错误的写法最开始我是这么写的var arr = [1,2,3,4,5] var reverse1 = function (arr) { let newArr = [] while (arr.length>0){ newArr.push(arr.pop()) } console.log(arr,newArr) // [],[5,4,3,2,1] arr = newArr //让arr等于反转后的新数组 console.log(arr) // [5,4,3,2,1] return arr } reverse1(arr)结果有点打脸console.log(arr) // []函数参数的传递方式上面的现象显示:我将arr当参数传入reverse1里面,我可以改变arr里面的内容但是我却无法改变arr(arr由[1,2,3,4,5]pop()五次变成[ ],但是随后的赋值操作却没有成功)为什么会出现这个问题,我们得从函数的参数传递方式说起函数的参数都是值传递怎么理解这句话,按照高程的说法,如果这里是引用传递,那么我在代码第8行已经让arr变成了[5,4,3,2,1],那么外面的arr也应该变,但是现在外面arr却是[ ],所以函数的参数的传递方法都是值传递如果你觉得这么说还是比较抽象,你可以听听鄙人陋见,我们完全可以把js里面数组名当成一个指针变量,储存的是实际的数组对象的地址。指针意味着我们通过它可以访问它指向的对象。变量意味者我可以改变这个变量。在函数参数里面当我们传入一个arr的时候,实际传递的是一个形参address1,储存arr这个对象在内存里的地址,address1=xxxx xxxxx xxxx xxx1。通过address1可以对arr进行任何操作,一旦现在我为address1赋上新的地址值,也就是address1=xxxx xxxxx xxxx xxx2。address1就和arr失去了联系。此时arr还是在xxxx xxxxx xxxx xxx1上,并且至少在当前的reverse1方法中不会再被改变了,因为没有哪个指针能指向它了。正确的写法ok说了这么多,实际想说的是,你在方法中可以通过索引,通过原生方法操作一个当参数传进来的数组,但是绝对不允许对数组名直接进行赋值那么这个题目我们就可以尝试使用原生方法来操作传进来的数组,这里提供一种思路var arr = [1,2,3,4,5]var reverse1 = function () { for(var i = 0; i < arr.length; i ++){ arr.splice(i,0,arr.pop()) } }reverse1(arr)console.log(arr) // [5,4,3,2,1]但是我们可以看到上面的方法还是不是很好,这是因为这个reverse1貌似只能对arr进行操作,耦合性太强,我们需要解耦,让reverse1对所有的数组都适用,这里适用this和原型方面的知识//一个完美的写法:Array.prototype.reverse1 = function () { for(var i = 0; i < this.length; i ++){ //this 解耦 this.splice(i,0,this.pop()) } return this }总结今天介绍了一下reverse的实现思路,this解耦,以及函数参数是值传递这么一个概念,建议在方法中通过索引,通过原生方法改变一个当参数传进来的数组,但是绝对不允许对数组名直接进行赋值希望对大家有所帮助。 ...

April 19, 2019 · 1 min · jiezi

JavaScript 中的 new 到底干了什么,跟原型链又有一些什么联系?

原文:https://legacy.ofcrab.com/press/javascript-new.html如果按面向对象的思路去讲 JavaScript 的 new,还是很难去理解,我们可以从另一个方向去理解一下它。你这些人类我是一名程序员,也是一个人,我可能:有一个响亮亮的名称在某一天出生是个男人我能行走我还能跑步还能跳跃能说话我还能写代码那么,在 JavaScript 中,我们可能像下面这样表达我:const me = { name: ‘大胡子农同工潘半仙’, birth: ‘1988-08-08’, sex: ‘male’, walk: function (speed, direction, duration) { // 以 speed 的速度向 direction 方向行走 duration 长的时间 }, run: function (speed, direction, duration) { // 像跑步一样,速度 }, jump: function (high, direction, angle) { // 以 angle 角度向 direction 方向跳 high 高 }, speak: function (letters) { // 说出 letters 这些词 }, coding: function (language, line) { // 写程序呢 }}你们这些人类当然,这个世界上不可能只有我一个程序员,更不可能只有我一个人,就像我们这个小公司,就有七八百人,似乎所有这些人的数据都保存在数据库里面:namesexbirth潘韬male1988-08-08高超male1985-08-09春雨male1999-08-08我们从数据库中查询出上面这几条记录,在 JavaScript 可能表示为一个二维数据,然后要创建出这三个人来,可能是下面这样的:const people = DB.query()// people = [[‘潘韬’, ‘male’, ‘1988-08-08’], […], […]]for (let i = 0; i < people.length; i++) { let [name, sex, birth] = people[i] people[i] = { name, sex, birth, walk: function () {}, run: function () {}, jump: function () {}, speak: function () {}, coding: function () {} }}重复的资源占用上面大家已经发现,像上面这样去创建三个对象, walk、run、jump、speak、coding 这五件能做的事情(方法),其实做法都一样,但是我们却重复的去描述该怎么做了,其实就占用了很多资源,所以,我们可能会像下面这样改进一下:const walk = function walk () {}const run = function run () {}const jump = function jump () {}const speak = function speak () {}const coding = function coding () {}for (let i = 0; i < people.length; i++) { let [name, sex, birth] = people[i] people[i] = { name, sex, birth, walk, run, jump, speak, coding }}不同的人共用相同的资源(方法)但是这个世界不止有人类对,人类相比于这个世界上的其它生物来讲,数量根本就值得一提,如果像上面这样,可能各种不同物种能做的事情都会要定义出不同的函数,蠕动肯定不是人类会去做的事情,但很多别的生物会做,那么为了代码管理方便,我们把人能做的所有事情都放在一个对象里面,这样就相当于有了一个命名空间了,不会再跟别的物种相冲突:const whatPeopleCanDo = { walk: function () {}, run: function () {}, jump: function () {}, speak: function () {}, coding: function () {}}for (let i = 0; i < people.length; i++) { let [name, sex, birth] = people[i] people[i] = { name, sex, birth, …whatPeopleCanDo }}原型但是,有的人可能我们并不知道他的 sex 信息是多少,有的也有可能不知道 birth 是多少,但是我们希望在创建这个人的时候,能给不知道的数据一些初始数据,所以, whatPeopleCanDo 并不能完全的表达出一个人,我们再改进:const peopleLike = { name: ‘’, sex: ‘unknown’, birth: ‘’, walk: function () {}, run: function () {}, jump: function () {}, speak: function () {}, coding: function () {}}for (let i = 0; i < people.length; i++) { let [name, sex, birth] = people[i] people[i] = { …peopleLike, name: name || peopleLike.name, sex: sex || peopleLike.sex, birth: birth || peopleLike.birth }}这样一来,我们就可以为不知道的属性加一些默认值,我们称 peopleLike 这个东东就为原型,它表示了像人类这样的物种有哪些属性,能干什么事情。原型链虽然上面已经比最开始的版本好得多了,但是还是能有很大的改进空间,我们现在像下面这样改一下:const peoplePrototype = { name: ‘’, sex: ‘unknown’, birth: ‘’, walk: function () {}, run: function () {}, jump: function () {}, speak: function () {}, coding: function () {}}for (let i = 0; i < people.length; i++) { let [name, sex, birth] = people[i] people[i] = { name: name || peoplePrototype.name, sex: sex || peoplePrototype.sex, birth: birth || peoplePrototype.birth, proto: peoplePrototype }}我们不再把人类原型里面的所有方法都绑定到某个人身上,而是像上面这样,用一个特殊的字段 proto 来指定:我的原型是 peoplePrototype 这个对象,同时,我们还制定了一个规则:如果你想请求我的某个方法,在我自己身上没有,那就去我的原型上面找吧,如果我的原型上面没有,那就去我的原型的原型上面去找,直到某个位置,没有更上层的原型为止像上面这样创建的 people 对象,有自己的属性,但是当我们去访问 people.speak() 方法的时候,其实访问的是 people.proto.speak(),这是我们的规则。更优雅的创建新新人类我们总不能在需要创建新人的时候,都像上面这样,自己去写一个对象,然后再手工指定它的原型是什么,所以,我们可以创建一个函数,专门用来生成人类的:const peoplePrototype = { name: ‘’, sex: ‘unknown’, birth: ‘’, walk: function () {}, run: function () {}, jump: function () {}, speak: function () {}, coding: function () {}}const makePeople = function makePeople(name, sex, birth) { let people = {} people.name = name || peoplePrototype.name people.sex = sex || peoplePrototype.sex people.birth = birth || peoplePrototype.birth people.proto = peoplePrototype return people}people = people.map(makePeople)现在这样我们只需要引入 makePeople 这个函数就可以随时随地创建新人了。更优雅一点的改进显然,上面这样并不是最好的办法,定义了一个原型,又定义了一个原型对象,我们可以把这两个合并到一起,所以,就可以有下面这样的实现了:const People = function People (name, sex, birth) { let people = {} people.name = name || People.prototype.name people.sex = sex || People.prototype.sex people.birth = birth || People.prototype.birth people.proto = People.prototype return people}People.prototype = { name: ‘’, sex: ‘unknown’, birth: ‘’, walk: function () {}, run: function () {}, jump: function () {}, speak: function () {}, coding: function () {}}我们直接把创建人类的那个函数叫作 People,这个函数有一个属性叫 prototype,它表示用我这个函数创建的对象的原型是什么,这个函数做的事情还是以前那些事儿,创建临时对象,设置对象的属性,绑定一下原型,然后返回。神奇的 this我们除了人,还有别的动物,比如 Tiger、Fish等,按上面的方式,在 Tiger() 或者 Fish() 函数里面都会建立不同的 tiger 或者 fish 名称的临时对象,这样太麻烦,我们把这种函数创建出来的对象,都可以统一叫作“这个对象” ,也就是 this object,不在关心是人是鬼,统一把所有的临时对象都叫 thisObject 或者更简单的就叫作:这个,即 this。const People = function People (name, sex, birth) { let this = {} this.name = name || People.prototype.name this.sex = sex || People.prototype.sex this.birth = birth || People.prototype.birth this.proto = People.prototype return this}当然,上面的这一段代码是有问题的,只是假想一样,这样是不是可行。new到现在为止,我们发现了整个代码的演变,是时候引出这个 new 了,它来干什么呢?它后面接一个类似上面这种 People 的函数,表示我需要创建一个 People 的实例,它的发明就是为了解决上面这些所有重复的事情,有了 new 之后,我们不需要再每一次定义一个临时对象,在 new 的上下文关系中,会在 People 函数体内自动为创建一个临时变量 this,这个就表示即将被创建出来的对象。同时,对于使用 new 创建的实例,会自动的绑定到创建函数的 prototype 作为原型,还会自动为 People 创建一个 constructor 函数,表示这个原型的创建函数是什么,所以,我们可以改成下面这样的了:const People = function People (name, sex, birth) { this.name = name || People.prototype.name this.sex = sex || People.prototype.sex this.birth = birth || People.prototype.birth}People.prototype.name = ‘‘People.prototype.sex = ‘unknown’People.prototype.birth = ‘‘People.prototype.walk = function () {}People.prototype.run = function () {}People.prototype.jump = function () {}People.prototype.speak = function () {}People.prototype.coding = function () {}people = people.map(p => new People(…p))总结new 到底干了什么?当 new People() 的时候创建临时变量 this,并将 this 绑定到 People 函数体里执行 People.prototype.constructor = People执行 this.proto = People.prototype执行 People 函数体中的自定义返回新创建的对象 ...

April 18, 2019 · 3 min · jiezi

JavaScript

前言作为前端高频面试题之一,相信很多小伙伴都有遇到过这个问题。那么你是否清楚完整的了解它呢? 国际惯例,让我们先抛出问题:什么是原型、原型链它们有什么特点它们能做什么怎么确定它们的关系或许你已经有答案,或许你开始有点疑惑,无论是 get 新技能或是简单的温习一次,让我们一起去探究一番吧如果文章中有出现纰漏、错误之处,还请看到的小伙伴多多指教,先行谢过以下↓原型JavaScript是基于原型的我们创建的每个函数都有一个 prototype(原型) 属性,这个属性是一个指针,指向一个对象,而这个对象的用途是包含可以由特定类型的所有实例共享的属性和方法。简单来说,就是当我们创建一个函数的时候,系统就会自动分配一个 prototype属性,可以用来存储可以让所有实例共享的属性和方法用一张图来表示就更加清晰了:图解:每一个构造函数都拥有一个 prototype 属性,这个属性指向一个对象,也就是原型对象原型对象默认拥有一个 constructor 属性,指向指向它的那个构造函数每个对象都拥有一个隐藏的属性 prototype,指向它的原型对象function Person(){}var person = new Person();person.proto === Person.prototype // truePerson.prototype.constructor === Person // true那么,原型对象都有哪些特点呢原型特点function Person(){}Person.prototype.name = ’tt’;Person.prototype.age = 18;Person.prototype.sayHi = function() { alert(‘Hi’);}var person = new Person();var person1 = new Person();person.name = ‘oo’;person.name // ooperson.age // 18perosn.sayHi() // Hiperson1.age // 18person1.sayHi() // Hi从这段代码我们不难看出:实例可以共享原型上面的属性和方法实例自身的属性会屏蔽原型上面的同名属性,实例上面没有的属性会去原型上面找既然原型也是对象,那我们可不可以重写这个对象呢?答案是肯定的function Person() {}Person.prototype = { name: ’tt’, age: 18, sayHi() { console.log(‘Hi’); }}var person = new Person()只是当我们在重写原型链的时候需要注意以下的问题:function Person(){}var person = new Person();Person.prototype = { name: ’tt’, age: 18}Person.prototype.constructor == Person // falseperson.name // undefined一图胜过千言万语在已经创建了实例的情况下重写原型,会切断现有实例与新原型之间的联系重写原型对象,会导致原型对象的 constructor 属性指向 Object ,导致原型链关系混乱,所以我们应该在重写原型对象的时候指定 constructor( instanceof 仍然会返回正确的值)Person.prototype = { constructor: Person}注意:以这种方式重设 constructor 属性会导致它的 Enumerable 特性被设置成 true(默认为false)既然现在我们知道了什么是 prototype(原型)以及它的特点,那么原型链又是什么呢?原型链JavaScript 中所有的对象都是由它的原型对象继承而来。而原型对象自身也是一个对象,它也有自己的原型对象,这样层层上溯,就形成了一个类似链表的结构,这就是原型链同样的,我们使用一张图来描述所有原型链的终点都是 Object 函数的 prototype 属性Objec.prototype 指向的原型对象同样拥有原型,不过它的原型是 null ,而 null 则没有原型清楚了原型链的概念,我们就能更清楚地知道属性的查找规则,比如前面的 person 实例属性.如果自身和原型链上都不存在这个属性,那么属性最终的值就是 undefined ,如果是方法就会抛出错误class类ES6 提供了 Class(类) 这个概念,作为对象的模板,通过 class 关键字,可以定义类为什么会提到 class :ES6 的 class 可以看作只是一个语法糖,它的绝大部分功能,ES5 都可以做到,新的 class 写法只是让对象原型的写法更加清晰、更像面向对象编程的语法而已class Point { constructor(x, y) { this.x = x; this.y = y; } toString() { return ‘(’ + this.x + ‘, ’ + this.y + ‘)’; }}// 可以这么改写function Point(x, y) { this.x = x; this.y = y;}Point.prototype.toString = function () { return ‘(’ + this.x + ‘, ’ + this.y + ‘)’;};class 里面定义的方法,其实都是定义在构造函数的原型上面实现实例共享,属性定义在构造函数中,所以 ES6 中的类完全可以看作构造函数的另一种写法除去 class 类中的一些行为可能与 ES5 存在一些不同,本质上都是通过原型、原型链去定义方法、实现共享。所以,还是文章开始那句话 JavaScript是基于原型的更多 class 问题,参考这里关系判断instanceof最常用的确定原型指向关系的关键字,检测的是原型,但是只能用来判断两个对象是否属于实例关系, 而不能判断一个对象实例具体属于哪种类型function Person(){}var person = new Person();person instanceof Person // trueperson instanceof Object // truehasOwnProperty通过使用 hasOwnProperty 可以确定访问的属性是来自于实例还是原型对象function Person() {}Person.prototype = { name: ’tt’}var person = new Person();person.age = 15;person.hasOwnProperty(‘age’) // trueperson.hasOwnProperty(’name’) // false原型链的问题由于原型链的存在,我们可以让很多实例去共享原型上面的方法和属性,方便了我们的很多操作。但是原型链并非是十分完美的function Person(){}Person.prototype.arr = [1, 2, 3, 4];var person1 = new Person();var person2 = new Person();person1.arr.push(5) person2.arr // [1, 2, 3, 4, 5]引用类型,变量保存的就是一个内存中的一个指针。所以,当原型上面的属性是一个引用类型的值时,我们通过其中某一个实例对原型属性的更改,结果会反映在所有实例上面,这也是原型 共享 属性造成的最大问题另一个问题就是我们在创建子类型(比如上面的 person)时,没有办法向超类型( Person )的构造函数中传递参数后记鉴于原型的特点和存在的问题,所以我们在实际开发中一般不会单独使用原型链。一般会使用构造函数和原型相配合的模式,当然这也就牵扯出了 JavaScript 中另一个有意思的领域:继承那么,什么又是继承呢且听下回分解最后,推荐一波前端学习历程,不定期分享一些前端问题和有意思的东西欢迎 star 关注 传送门参考文档JavaScript高级程序设计ECMAScript6入门 ...

April 17, 2019 · 2 min · jiezi

ES5的用对象实现类的作用

// js实现类 ES5// 由于函数始对象 所以写法不是唯一// function Pf(){}更符合固有思想// 由于ES6添加了类,所以书写变得跟方便var Pf = function(name,age){ // 私有属性方法外面不能调用,只有对象方法可以操作,从而起到保护数据的作用 // 私有属性 var secret = ‘小秘密’ // 私有方法 function secretfn(){ console.log(‘私有方法’) console.log(“my secret is”+secret) } // 公共属性方法是每创建一个对象就会创建一个该属性或方法(耗费一定把内存) // 共有实例属性 this.name = name this.age = age //共有实例方法 this.say = function(){ console.log(“my name is”+this.name,“my age is”+this.age) console.log(‘可以操作私有属性与方法’) secretfn() }}// 静态方法Pf.f1 = function(){ console.log(‘我是静态方法,只能用类直接调用,实例对象不能调用’)}Pf.prototype = { constructor:Pf,// 这种添加原型方法需要重置制定对象。 // 原型链上的方法为公有方法,由类创建出来的对象会指向该原型,不会重新创建该方法,但是优先级没有对象方法高 // 其优点是节省内存 say:function(){ console.log(“原型上的say”) // 原型链上可以拿到共有属性,拿不到私有属性与方法 console.log(‘我也能拿到数据’+this.name) } }var a = new Pf(‘ss’,22)a.say()Pf.f1()运行直接node ...

April 14, 2019 · 1 min · jiezi

javascript 面向对象(实现继承的几种方式)

1、原型链继承核心: 将父类的实例作为子类的原型缺点: 父类新增原型方法/原型属性,子类都能访问到,父类一变其它的都变了 function Person (name) { this.name = name; }; Person.prototype.getName = function () { //对原型进行扩展 return this.name; }; function Parent (age) { this.age = age; }; Parent.prototype = new Person(‘老明’); //这一句是关键 //通过构造器函数创建出一个新对象,把老对象的东西都拿过来。 Parent.prototype.getAge = function () { return this.age; };// Parent.prototype.getName = function () { //可以重写从父类继承来的方法,会优先调用自己的。// console.log(222);// }; var result = new Parent(22); console.log(result.getName()); //老明 //调用了从Person原型中继承来的方法(继承到了当前对象的原型中) console.log(result.getAge()); //22 //调用了从Parent原型中扩展来的方法2、构造继承基本思想借用构造函数的基本思想就是利用call或者apply把父类中通过this指定的属性和方法复制(借用)到子类创建的实例中。因为this对象是在运行时基于函数的执行环境绑定的。也就是说,在全局中,this等于window,而当函数被作为某个对象的方法调用时,this等于那个对象。call、apply 方法可将一个函数的对象上下文从初始的上下文改变为由 thisObj 指定的新对象。所以,这个借用构造函数就是,new对象的时候(new创建的时候,this指向创建的这个实例),创建了一个新的实例对象,并且执行Parent里面的代码,而Parent里面用call调用了Person,也就是说把this指向改成了指向新的实例,所以就会把Person里面的this相关属性和方法赋值到新的实例上,而不是赋值到Person上面,所以所有实例中就拥有了父类定义的这些this的属性和方法。因为属性是绑定到this上面的,所以调用的时候才赋到相应的实例中,各个实例的值就不会互相影响了。核心:使用父类的构造函数来增强子类实例,等于是复制父类的实例属性给子类(没用到原型)缺点: 方法都在构造函数中定义, 只能继承父类的实例属性和方法,不能继承原型属性/方法,无法实现函数复用,每个子类都有父类实例函数的副本,影响性能 function Person (name) { this.name = name; this.friends = [‘小李’,‘小红’]; this.getName = function () { return this.name; } };// Person.prototype.geSex = function () { //对原型进行扩展的方法就无法复用了// console.log(“男”);// }; function Parent = (age) { Person.call(this,‘老明’); //这一句是核心关键 //这样就会在新parent对象上执行Person构造函数中定义的所有对象初始化代码, // 结果parent的每个实例都会具有自己的friends属性的副本 this.age = age; }; var result = new Parent(23); console.log(result.name); //老明 console.log(result.friends); //[“小李”, “小红”] console.log(result.getName()); //老明 console.log(result.age); //23 console.log(result.getSex()); //这个会报错,调用不到父原型上面扩展的方法3、组合继承组合继承(所有的实例都能拥有自己的属性,并且可以使用相同的方法,组合继承避免了原型链和借用构造函数的缺陷,结合了两个的优点,是最常用的继承方式)核心:通过调用父类构造,继承父类的属性并保留传参的优点,然后再通过将父类实例作为子类原型,实现函数复用缺点:调用了两次父类构造函数,生成了两份实例(子类实例将子类原型上的那份屏蔽了) function Person (name) { this.name = name; this.friends = [‘小李’,‘小红’]; }; Person.prototype.getName = function () { return this.name; }; function Parent (age) { Person.call(this,‘老明’); //这一步很关键 this.age = age; }; Parent.prototype = new Person(‘老明’); //这一步也很关键 var result = new Parent(24); console.log(result.name); //老明 result.friends.push(“小智”); // console.log(result.friends); //[‘小李’,‘小红’,‘小智’] console.log(result.getName()); //老明 console.log(result.age); //24 var result1 = new Parent(25); //通过借用构造函数都有自己的属性,通过原型享用公共的方法 console.log(result1.name); //老明 console.log(result1.friends); //[‘小李’,‘小红’]4、寄生组合继承核心:通过寄生方式,砍掉父类的实例属性,这样,在调用两次父类的构造的时候,就不会初始化两次实例方法/属性,避免的组合继承的缺点缺点:堪称完美,但实现较为复杂 function Person(name) { this.name = name; this.friends = [‘小李’,‘小红’]; } Person.prototype.getName = function () { return this.name; }; function Parent(age) { Person.call(this,“老明”); this.age = age; } (function () { var Super = function () {}; // 创建一个没有实例方法的类 Super.prototype = Person.prototype; Parent.prototype = new Super(); //将实例作为子类的原型 })(); var result = new Parent(23); console.log(result.name); console.log(result.friends); console.log(result.getName()); console.log(result.age); ...

April 2, 2019 · 2 min · jiezi

关于构造函数、原型、原型链、多种方式继承

构造函数与实例对象又是这个经典的问题,嗯,我先来写个构造函数,然后实例化一个对象看看。function Person(name) { this.name = name}Person.prototype.eat = () => {console.log(’eat’)}Person.prototype.play = () => {console.log(‘play’)}let Han = new Person(‘Han’)通过一系列打印发现了这样的关系:原型链 – 原型(prototype)和隐式原型(proto)可以看出实例对象没有prototype(也就是原型),只有构造器才拥有原型。而所有的js对象都拥有__proto__(也就是隐式原型),这个隐式原型所指向的就是创造这个对象的构造器的原型。如实例Han的隐式原型指向了其构造函数(Person)的原型;Person的隐式原型指向了Function的原型;而原型自身也有隐式原型,指向了Object的原型。有点绕口,其实就是通过隐式原型可以向上找到是谁构造了自己,并且如果自己没有相应的属性或者方法,可以沿着这条原型链向上找到最近的一个属性或方法来调用。如Han.eat(),实际上是调用了Han.proto.eat(),把构造器Person的原型的eat方法给拿来用了;再如Han.hasOwnProperty(’name’),实际上是调用了Han.proto.proto.hasOwnProperty(’name’),因为Han自己没hasOwnProperty这方法,就通过隐式原型向上找到了Person的原型,发现也没这方法,就只能再沿着Person的原型的隐式原型向上找到了Object的原型,嗯然后发现有这方法就拿来调用了。构造器constructor所有构造函数都有自己的原型(prototype),而原型一定有constructor这么个属性,指向构造函数本身。也就是告诉大家这个原型是属于本构造函数的。Function & Object可以看出Person这个构造函数是由Function创建出来的,而我们看下Function的隐式原型,竟然是指向了Function的原型,也就是Function也是由Function创建出来的。很绕是不是,我们先不管,继续溯源下去,再看下Function的原型的隐式原型,指向的是Object的原型,继续往上找Object的原型的隐式原型,嗯终于结束了找到的是null,也就是Object的原型是原型链上的最后一个元素了。接下来看下Object,Object是由Function创建出来的,而Function的隐式原型的隐式原型是Object的原型也就是Function通过原型链可以向上找到Object的原型,两者看起来是你生我我生你的关系,这里也就引用比较好懂的文章来解释下: 从Object和Function说说JS的原型链ObjectJavaScript中的所有对象都来自Object;所有对象从Object.prototype继承方法和属性,尽管它们可能被覆盖。例如,其他构造函数的原型将覆盖constructor属性并提供自己的toString()方法。Object原型对象的更改将传播到所有对象,除非受到这些更改的属性和方法将沿原型链进一步覆盖。FunctionFunction 构造函数 创建一个新的Function对象。 在 JavaScript 中, 每个函数实际上都是一个Function对象。—- 来自mozilla接下来说下构造函数实例化对象到底做了些啥,其实看也能看出来了。let Jan = {}Person.call(Jan, ‘Jan’)Jan.proto = Person.prototype1、创建一个空对象。2、将构造函数的执行对象this赋给这个空对象并且执行。3、把对象的隐式原型指向构造函数的原型。4、返回这个对象是的就是这样,next page!继承原型链继承function Person(name) { this.name = name this.skills = [’eat’, ‘sleep’]}Person.prototype.say = ()=> {console.log(‘hi’)}function Boss() {}Boss.prototype = new Person()let Han = new Boss()原理就是这样????子构造函数的原型指向了父构造函数的实例对象,因此子构造函数的实例对象可以通过原型链找到父构造函数的原型方法和类属性。优点:所有实例对象都可以共享父构造函数的原型方法。缺点:1、父构造函数的引用属性也被共享了,相当于所有的实例对象只要对自身的skills属性进行修改都会引发共振,因为其实修改的是原型链上的skills属性。当然对skills重新赋值可以摆脱这一枷锁,相当于自身新建了skills属性来覆盖了原型链上的。2、实例化时无法对父构造函数传参。3、子构造函数原型中的constructor不再是子类自身,而是通过原型链找到了父类的constructor。构造函数继承function Person(name) { this.name = name this.skills = [’eat’, ‘sleep’]}Person.prototype.say = ()=> {console.log(‘hi’)}function Boss(name) { Person.call(this, name)}let Han = new Boss(‘Han’)原理就是父构造函数把执行对象赋给子构造函数的实例对象后执行自身。优点:1、实例化时可以对父构造函数传参。2、父类的引用属性不会被共享。3、子构造函数原型中的constructor还是自身,原型没有被修改。缺点:每次实例化都执行了一次父构造函数,子类不能继承父类的原型,如果把父类原型上的方法写在父类的构造函数里,虽然子类实例对象可以调用父类的方法,但父类的方法是单独加在每个实例对象上,会造成性能的浪费。组合继承结合了原型链继承和构造函数继承两种方法。function Person(name) { this.name = name this.skills = [’eat’, ‘sleep’]}Person.prototype.say = ()=> {console.log(‘hi’)}function Boss(name, age) { Person.call(this, name) this.age = age}Boss.prototype = new Person()Boss.prototype.constructor = BossBoss.prototype.sleep = ()=> {console.log(‘sleep’)}let Han = new Boss(‘Han’, 18)看起来是完美解决了一切。但就是????实例化的对象实际上是用构造函数继承的方法往自己身上加属性从而覆盖原型链上的相应属性的,既然如此,为什么不直接那父构造器的原型加到子构造器的原型上呢?这样就不会出现那多余的父类实例化对象出来的属性了。function Person(name) { this.name = name this.skills = [’eat’, ‘sleep’]}Person.prototype.say = ()=> {console.log(‘hi’)}function Boss(name, age) { Person.call(this, name) this.age = age}Boss.prototype = Person.prototype //modifiedBoss.prototype.constructor = BossBoss.prototype.sleep = ()=> {console.log(‘sleep’)}let Han = new Boss(‘Han’, 18)看起来很是完美,反正效果是达到了,性能优化也是最佳。但问题就是这样一点继承关系都看不出来啊,父类和子类的原型完全融合在一块了,一点都不严谨。所以最优的继承方式应该是。。。寄生组合继承function Person(name) { this.name = name this.skills = [’eat’, ‘sleep’]}Person.prototype.say = ()=> {console.log(‘hi’)}function Boss(name, age) { Person.call(this, name) this.age = age}Boss.prototype = Object.create(Person.prototype)Boss.prototype.sleep = ()=> {console.log(‘sleep’)}Boss.prototype.constructor = Bosslet Han = new Boss(‘Han’, 18)先看图????其实跟组合继承有点像,构造函数继承部分和组合继承的一样就不说了。原型链那块和原型链继承有所不同的是原型链继承是直接拿了父类的实例对象来作为子类的原型,而这里是用以父类的原型为原型的构造函数实例化出来的对象作为子类的原型(Object.create做的事情),完美避开了不必要的父类构造函数里的东西。Object.create()方法创建一个新对象,使用现有的对象来提供新创建的对象的__proto__。相当于这样????function create(proto) { function F() {} F.prototype = proto return new F()}听说ES6的class extend也是这么做的,更多的继承细节可以看看这篇文章,本继承章节也参考了的????一篇文章理解JS继承——原型链/构造函数/组合/原型式/寄生式/寄生组合/Class extends ...

March 16, 2019 · 1 min · jiezi

深入学习js之——原型和原型链

开篇:在Brendan Eich大神为JavaScript设计面向对象系统的时候,借鉴了Self 和Smalltalk这两门基于原型的语言,之所以选择基于原型的面向对象系统,并不是因为时间匆忙,它设计起来相对简单,而是因为从一开始Brendan Eich就没打算在Javascipt中加入类的概念。以类为中心的面向对象的编程语言中,类和对象的关系可以想象成铸模和铸件的关系,对象总是从类中创建而来,而在原型编程的思想中,类并不是必须的,对象未必需要从一个类中创建而来。JavaScript是一门完全面向对象的语言,如果想要更好地使用JavaScript的面向对象系统,原型和原型链就是个绕不开的话题,今天我们就一起来学习一下这方面的知识。理解三个重要的属性:prototype、proto、constructor见名知意,所谓的"链"描述的其实是一种关系,加上原型两个字,可以理解为原型之间的关系,既然是一种关系,就需要维系,就好比我们走亲访友,亲情就是一种纽带,类比在JavaScript当中——函数、对象实例、实例原型也有自身的联系,而他们之间的纽带就是下面这三个重要的属性:三个重要的属性:prototype、proto、constructorprototype我们先来看看第一个属性:prototype所谓属性,指的是一个事物的特征,就比如美女的一大特征是“大长腿”,那“大长腿"就是美女的属性,类比到JavaScript中函数,每一个函数都有一个prototype属性,这属性就是与生俱来的特质。这里需要特别强调一下,是函数,普通的对象是没有这个属性的,(这里为什么说普通对象呢,因为在JavaScript里面,一切皆为对象,所以这里的普通对象不包括函数对象)我们来看一个例子:function Person() {}// 虽然写在注释里面,但是需要注意的是// prototype 是函数才会有的属性 (哈哈哈,看来在JavaScript中函数果然是有特权的……)Person.prototype.name = “Kevin”;var person1 = new Person();var person2 = new Person();console.log(person1.name) // Kevinconsole.log(person2.name) // Kevin上面的代码中我们创建了一个构造函数Person,并且在实例原型上面添加了一个name属性赋值为"Kevin";然后分别创建了两个实例对象:person1、person2;当我们打印两个实例对象上name属性时均输出了Kevin(可以亲自试一下)。我们不禁疑惑,这个Person.prototype到底是什么,为什么在上面添加属性,在构造函数的实例化对象上都能访问到呢?其实 Person这个函数的prototype属性指向了一个对象,即:Person.prototype也是一个对象。(真是好多对象)这个对象正是调用该构造函数而创建的实例的原型。也就是这个例子中的person1和person2的原型。为了便于理解,我们将上面的这段话拆解一下:1.调用的构造函数: Person2.使用什么调用: new关键字3.得到了什么: 两个实例化对象person1、person24.实例化对象和原型是什么关系: person1和person2的原型就是 Person.prototype那什么是原型呢?可以这样理解:每一个JavaScript对象(null除外)在创建的时候就会与之关联另外一个对象,这个对象就是我们所说的原型,而每一个对象都会从原型"继承"属性。上面的代码中我们并没有直接在person1和person2中添加name属性 但是这两个对象却能够访问name属性,就是这个道理。我们用一张图表示构造函数和实例原型之间的关系:好了 构造函数和实例原型之间的关系我们已经梳理清楚了,那我们怎么表示实例与实例原型,也就是person1或者person2和Person.prototype 之间的关系呢。这时候需要请出我们理解原型链的第二个重要属性__proto____proto__这个属性有什么特征呢?其实这是每一个JavaScript对象(除了null)都具有的一个属性,叫__proto__,这个属性会指向该对象的原型,即作为实例对象和实例原型的之间的链接桥梁,这里强调,是对象,同样,因为函数也是对象,所以函数也有这个属性。我们看一个代码示例:function Person() {}var person = new Person();console.log(person.proto === Person.prototype); //true;有了第二个属性的帮助,我们就能更加全面的理解这张关系图了:通过上面的关系图我们可以看到,构造函数Person 和实例对象person 分别通过prototype和__proto__ 和实例原型Person.prototype进行关联,根据箭头指向我们不禁要有疑问:实例原型是否有属性指向构造函数或者实例呢?这时候该请出我们的第三个属性了:constructorconstructor实例原型指向实例的属性倒是没有,因为一个构造函数可能会生成很多个实例,但是原型指向构造函数的属性倒是有的,这就是我们的constructor——每一个原型都有一个constructor属性指向关联的构造函数。我们再来看一个示例:function Person() {}console.log(Person === Person.prototype.constructor); // true好了到这里我们再完善下关系图:通过对三个属性的介绍,我们总结一下:function Person() {}var person = new Person();console.log(person.proto == Person.prototype) // trueconsole.log(Person.prototype.constructor == Person) // true// 顺便学习一个ES5的方法,可以获得对象的原型console.log(Object.getPrototypeOf(person) === Person.prototype) // true上述代码中我们我们执行了以下操作:1.声明了构造函数 Person;2.使用new操作符调用 Person 实例化了一个person 对象;3.判断实例化对象通过__proto__是否指向实例原型;4.判断实例原型通过constructor是否能找到对应的构造函数;5.使用Object.getPrototypeOf方法传入一个对象 找到对应的原型对象;了解了构造函数。实例原型、和实例对象之间的关系,接下来我们讲讲实例和原型的关系:实例与原型当读取实例的属性时,如果找不到,就会查找与对象关联的原型中的属性,如果还查不到,就去找原型的原型,一直找到最顶层为止。我们再举一个例子: function Person() { } Person.prototype.name = ‘Kevin’; var person = new Person(); person.name = ‘Daisy’; console.log(person.name) // Daisy delete person.name; console.log(person.name) // Kevin在上面这个例子中,我们给实例person添加了name 属性,当我们打印person.name的时候,结果自然为Daisy但是当我们删除了person下面的name属性后,读取person.name,依然能够成功输出Kevin,实际情况是从 person 对象中找不到 name 属性就会从 person 的原型也就是 person.proto ,也就是 Person.prototype中查找,幸运的是我们找到了 name 属性,结果为 Kevin。但是我们不禁有疑问,如果万一没有找到该怎么办?我们来看下一层的关系 原型的原型原型的原型我们前面提到过,原型也是一个对象,那么既然是对象,那肯定就有创建它的构造函数,这个构造函数就是Object(); var obj = new Object(); obj.name = ‘Kevin’; console.log(obj.name); // Kevin;其实原型对象就是通过Object构造函数生成的,结合之前我们所说的,实例__proto__指向构造函数的prototype 所以我们再丰富一下我们的关系图;到了这里我们对于 构造函数、实例对象、实例原型之间的关系又有了进一步的认识。说了这么多,终于可以介绍原型链了。原型链那Object.prototype 的原型呢?Object是根节点的对象,再往上查找就是null,我们可以打印: console.log(Object.prototype.proto === null) // true然而 null 究竟代表了什么呢?引用阮一峰老师的 《undefined与null的区别》 就是:null 表示“没有对象”,即该处不应该有值。所以 Object.prototype.proto 的值为 null 跟 Object.prototype 没有原型,其实表达了一个意思。所以查找属性的时候查到 Object.prototype 就可以停止查找了。我们可以将null 也加入最后的关系图中,这样就比较完整了。上图中相互关联的原型组成的链状结构就是原型链,也就是红色的这条线补充最后,补充三点大家可能不会注意到的地方:constructor首先是constructor,我们看一个例子:function Person() {}var person = new Person();console.log(person.constructor === Person); // true当获取person.constructor时,其实 person 中并没有constructor 属性,当不能读取到constructor属性时,会从 person 的原型也就是 Person.prototype中读取,正好原型中有该属性,所以:person.constructor === Person.prototype.constructor__proto__其次是 proto ,绝大部分浏览器都支持这个非标准的方法访问原型,然而它并不存在于 Person.prototype 中,实际上,它是来自于 Object.prototype ,与其说是一个属性,不如说是一个 getter/setter,当使用 obj.proto 时,可以理解成返回了 Object.getPrototypeOf(obj)。真的是继承吗?最后是关于继承,前面我们讲到“每一个对象都会从原型‘继承’属性”,实际上,继承是一个十分具有迷惑性的说法,引用《你不知道的JavaScript》中的话,就是:继承意味着复制操作,然而 JavaScript 默认并不会复制对象的属性,相反,JavaScript 只是在两个对象之间创建一个关联,这样,一个对象就可以通过委托访问另一个对象的属性和函数,所以与其叫继承,委托的说法反而更准确些。参考:1、《Javascript设计模式与开发实践》2、JavaScript深入之从原型到原型链欢迎添加我的个人微信讨论技术和个体成长。欢迎关注我的个人微信公众号——指尖的宇宙,更多优质思考干货 ...

February 24, 2019 · 1 min · jiezi

Javascript 原型链

先来一张图看看几个名词的关系 构造函数、原型、实例原谅我的狂草字体,我手写比用电脑画快。今天我们只说原型链,所以接下来我就围绕着原型链的几个部分说起。这个大家都很熟悉了,首字母大写的函数我们都可以作为构造函数,不是说小写的就不能new,也是可以的,暂时说成约定俗成吧! // 构造函数 function Fn() {} //原型对象 console.log(Fn.prototype) //new let fn = new Fn() //实例 console.log(fn)代码部分结束了,今天我们就用这4行代码描述一下上图也就是原型链的来龙去脉。每个函数都有一个属性prototype,借用Function.prototype 属性存储了 Function 的原型对象。验证了我草图构造函数.prototype指向原型对象完整的log看一下实例.__proto__也指向原型对象从log里也能看出来实例原型的constructor指向构造函数最后再说构造函数 new关键字生成实例手绘图说完了,我们说正题 原型链,为什么再说原型链之前先画了一个草图,为了帮助预热理解。每一个原型对象都有一个__proto__属性,这个是我们在代码中继承的关键,也是众多面试官所问的什么是原型链上图可以看到,第一次__proto__找到了原型对象,第二次__proto__找到了Object实例对象,第三次null查找结束。我们平时开发中用到了__proto__去查找链条中我们继承的方法和属性都在prototype(原型)上,所以不能在Fn.prototype = xxx操作,这样链条就会中断,只能在原型上扩展属性。明天继续聊继承!欢迎吐槽!

February 18, 2019 · 1 min · jiezi

深入学习js之——原型和原型链

开篇:在Brendan Eich大神为JavaScript设计面向对象系统的时候,借鉴了Self 和Smalltalk这两门基于原型的语言,之所以选择基于原型的面向对象系统,并不是因为时间匆忙,它设计起来相对简单,而是因为从一开始Brendan Eich就没打算在Javascipt中加入类的概念。以类为中心的面向对象的编程语言中,类和对象的关系可以想象成铸模和铸件的关系,对象总是从类中创建而来,而在原型编程的思想中,类并不是必须的,对象未必需要从一个类中创建而来。JavaScript是一门完全面向对象的语言,如果想要更好地使用JavaScript的面向对象系统,原型和原型链就是个绕不开的话题,今天我们就一起来学习一下这方面的知识。理解三个重要的属性:prototype、proto、constructor见名知意,所谓的"链"描述的其实是一种关系,加上原型两个字,可以理解为原型之间的关系,既然是一种关系,就需要维系,就好比我们走亲访友,亲情就是一种纽带,类比在JavaScript当中——函数、对象实例、实例原型也有自身的联系,而他们之间的纽带就是下面这三个重要的属性:三个重要的属性:prototype、proto、constructorprototype我们先来看看第一个属性:prototype所谓属性,指的是一个事物的特征,就比如美女的一大特征是“大长腿”,那“大长腿"就是美女的属性,类比到JavaScript中函数,每一个函数都有一个prototype属性,这属性就是与生俱来的特质。这里需要特别强调一下,是函数,普通的对象是没有这个属性的,(这里为什么说普通对象呢,因为在JavaScript里面,一切皆为对象,所以这里的普通对象不包括函数对象)我们来看一个例子:function Person() {}// 虽然写在注释里面,但是需要注意的是// prototype 是函数才会有的属性 (哈哈哈,看来在JavaScript中函数果然是有特权的……)Person.prototype.name = “Kevin”;var person1 = new Person();var person2 = new Person();console.log(person1.name) // Kevinconsole.log(person2.name) // Kevin上面的代码中我们创建了一个构造函数Person,并且在实例原型上面添加了一个name属性赋值为"Kevin";然后分别创建了两个实例对象:person1、person2;当我们打印两个实例对象上name属性时均输出了Kevin(可以亲自试一下)。我们不禁疑惑,这个Person.prototype到底是什么,为什么在上面添加属性,在构造函数的实例化对象上都能访问到呢?其实 Person这个函数的prototype属性指向了一个对象,即:Person.prototype也是一个对象。(真是好多对象)这个对象正是调用该构造函数而创建的实例的原型。也就是这个例子中的person1和person2的原型。为了便于理解,我们将上面的这段话拆解一下:1.调用的构造函数: Person2.使用什么调用: new关键字3.得到了什么: 两个实例化对象person1、person24.实例化对象和原型是什么关系: person1和person2的原型就是 Person.prototype那什么是原型呢?可以这样理解:每一个JavaScript对象(null除外)在创建的时候就会与之关联另外一个对象,这个对象就是我们所说的原型,而每一个对象都会从原型"继承"属性。上面的代码中我们并没有直接在person1和person2中添加name属性 但是这两个对象却能够访问name属性,就是这个道理。我们用一张图表示构造函数和实例原型之间的关系:好了 构造函数和实例原型之间的关系我们已经梳理清楚了,那我们怎么表示实例与实例原型,也就是person1或者person2和Person.prototype 之间的关系呢。这时候需要请出我们理解原型链的第二个重要属性__proto____proto__这个属性有什么特征呢?其实这是每一个JavaScript对象(除了null)都具有的一个属性,叫__proto__,这个属性会指向该对象的原型,即作为实例对象和实例原型的之间的链接桥梁,这里强调,是对象,同样,因为函数也是对象,所以函数也有这个属性。我们看一个代码示例:function Person() {}var person = new Person();console.log(person.proto === Person.prototype); //true;有了第二个属性的帮助,我们就能更加全面的理解这张关系图了:通过上面的关系图我们可以看到,构造函数Person 和实例对象person 分别通过prototype和__proto__ 和实例原型Person.prototype进行关联,根据箭头指向我们不禁要有疑问:实例原型是否有属性指向构造函数或者实例呢?这时候该请出我们的第三个属性了:constructorconstructor实例原型指向实例的属性倒是没有,因为一个构造函数可能会生成很多个实例,但是原型指向构造函数的属性倒是有的,这就是我们的constructor——每一个原型都有一个constructor属性指向关联的构造函数。我们再来看一个示例:function Person() {}console.log(Person === Person.prototype.constructor); // true好了到这里我们再完善下关系图:通过对三个属性的介绍,我们总结一下:function Person() {}var person = new Person();console.log(person.proto == Person.prototype) // trueconsole.log(Person.prototype.constructor == Person) // true// 顺便学习一个ES5的方法,可以获得对象的原型console.log(Object.getPrototypeOf(person) === Person.prototype) // true上述代码中我们我们执行了以下操作:1.声明了构造函数 Person;2.使用new操作符调用 Person 实例化了一个person 对象;3.判断实例化对象通过__proto__是否指向实例原型;4.判断实例原型通过constructor是否能找到对应的构造函数;5.使用Object.getPrototypeOf方法传入一个对象 找到对应的原型对象;了解了构造函数。实例原型、和实例对象之间的关系,接下来我们讲讲实例和原型的关系:实例与原型当读取实例的属性时,如果找不到,就会查找与对象关联的原型中的属性,如果还查不到,就去找原型的原型,一直找到最顶层为止。我们再举一个例子: function Person() { } Person.prototype.name = ‘Kevin’; var person = new Person(); person.name = ‘Daisy’; console.log(person.name) // Daisy delete person.name; console.log(person.name) // Kevin在上面这个例子中,我们给实例person添加了name 属性,当我们打印person.name的时候,结果自然为Daisy但是当我们删除了person下面的name属性后,读取person.name,依然能够成功输出Kevin,实际情况是从 person 对象中找不到 name 属性就会从 person 的原型也就是 person.proto ,也就是 Person.prototype中查找,幸运的是我们找到了 name 属性,结果为 Kevin。但是我们不禁有疑问,如果万一没有找到该怎么办?我们来看下一层的关系 原型的原型原型的原型我们前面提到过,原型也是一个对象,那么既然是对象,那肯定就有创建它的构造函数,这个构造函数就是Object(); var obj = new Object(); obj.name = ‘Kevin’; console.log(obj.name); // Kevin;其实原型对象就是通过Object构造函数生成的,结合之前我们所说的,实例__proto__指向构造函数的prototype 所以我们再丰富一下我们的关系图;到了这里我们对于 构造函数、实例对象、实例原型之间的关系又有了进一步的认识。说了这么多,终于可以介绍原型链了。原型链那Object.prototype 的原型呢?Object是根节点的对象,再往上查找就是null,我们可以打印: console.log(Object.prototype.proto === null) // true然而 null 究竟代表了什么呢?引用阮一峰老师的 《undefined与null的区别》 就是:null 表示“没有对象”,即该处不应该有值。所以 Object.prototype.proto 的值为 null 跟 Object.prototype 没有原型,其实表达了一个意思。所以查找属性的时候查到 Object.prototype 就可以停止查找了。我们可以将null 也加入最后的关系图中,这样就比较完整了。上图中相互关联的原型组成的链状结构就是原型链,也就是红色的这条线补充最后,补充三点大家可能不会注意到的地方:constructor首先是constructor,我们看一个例子:function Person() {}var person = new Person();console.log(person.constructor === Person); // true当获取person.constructor时,其实 person 中并没有constructor 属性,当不能读取到constructor属性时,会从 person 的原型也就是 Person.prototype中读取,正好原型中有该属性,所以:person.constructor === Person.prototype.constructor__proto__其次是 proto ,绝大部分浏览器都支持这个非标准的方法访问原型,然而它并不存在于 Person.prototype 中,实际上,它是来自于 Object.prototype ,与其说是一个属性,不如说是一个 getter/setter,当使用 obj.proto 时,可以理解成返回了 Object.getPrototypeOf(obj)。真的是继承吗?最后是关于继承,前面我们讲到“每一个对象都会从原型‘继承’属性”,实际上,继承是一个十分具有迷惑性的说法,引用《你不知道的JavaScript》中的话,就是:继承意味着复制操作,然而 JavaScript 默认并不会复制对象的属性,相反,JavaScript 只是在两个对象之间创建一个关联,这样,一个对象就可以通过委托访问另一个对象的属性和函数,所以与其叫继承,委托的说法反而更准确些。参考:1、《Javascript设计模式与开发实践》2、JavaScript深入之从原型到原型链后记:欢迎大家关注我的微信公众号,更多优质文章第一时间推送: ...

January 30, 2019 · 1 min · jiezi

【前端面试】原型和原型链

1.题目如何准确判断一个变量是数组写一个原型链继承的例子继承实现的其他方式es6 实现继承的底层原理是什么描述new一个对象的过程zepto及其他源码中如何使用原型链2.知识点2.1 构造函数特点:以大写字母开头function Foo(name,age){ //var obj = {} //this = {} this.name = name; this.age = age; this.class = ‘class1’ // return this}var f1 = new Foo(’liming’,19);扩展var o = {} 是 var o = new Object() 的语法糖var a = [] 是 var a = new Array() 的语法糖function Foo(){} 相当于 var Foo = new Function(){}2.2 原型规则五条规则:1.所有引用类型(对象,数组,函数)都具有对象特性,即可以自由扩展属性2.所有引用类型(对象,数组,函数)都具有一个__proto__(隐式原型)属性,是一个普通对象3.所有的函数都具有prototype(显式原型)属性,也是一个普通对象4.所有引用类型(对象,数组,函数)proto__值指向它构造函数的prototype5.当试图得到一个对象的属性时,如果变量本身没有这个属性,则会去他的__proto__中去找for (var key in object) { //高级浏览器中已经屏蔽了来自原型的属性 //建议加上判断保证程序的健壮性 if (object.hasOwnProperty(key)) { console.log(object[key]); }}2.3 原型链obj. proto . proto . proto __ … Object.prototype === nullinstanceof 用于判断引用类型属于哪个构造函数obj instanceob Foo实际意义:判断 Foo.prototype 在不在 obj的原型链上3.题目解答3.1 如何准确判断一个变量是数组arr instanceof Array3.2 写一个原型链继承的例子封装dom查询function Elem(id){ this.elem = document.getElementById(id);};Elem.prototype.html = function(val){ var elem = this.elem; if (val) { elem.innerHTML = val; return this; }else{ return elem.innerHTML; }}Elem.prototype.on = function(type,fun){ var elem = this.elem; elem.addEventListener(type,fun); return this;}var div1 = new Elem(‘id1’);div1.html(“test”).on(‘click’,function(){ console.log(‘点击’);})3.3 继承实现的其他方式3.3.1 原型继承 var obj = { 0:‘a’, 1:‘b’, arr:[1] } function Foo(arr2){ this.arr2 = [1] } Foo.prototype = obj; var foo1 = new Foo(); var foo2 = new Foo(); foo1.arr.push(2); foo1.arr2.push(2); console.log(foo2.arr); //[1,2] console.log(foo2.arr2); //[1]优点:实现简单缺点:1.无法向父类构造函数传参2.同时new两个对象时改变一个对象的原型中的引用类型的属性时,另一个对象的该属性也会修改。因为来自原型对象的引用属性是所有实例共享的。3.3.2 构造继承 function Super(b){ this.b = b; this.fun = function(){} } function Foo(a,b){ this.a = a; Super.call(this,b); } var foo1 = new Foo(1,2); console.log(foo1.b);优点:可以向父类传参,子类不会共享父类的引用属性缺点:无法实现函数复用,每个子类都有新的fun,太多了就会影响性能,不能继承父类的原型对象。3.3.3 组合继承function Super(){ // 只在此处声明基本属性和引用属性 this.val = 1; this.arr = [1];}// 在此处声明函数Super.prototype.fun1 = function(){};Super.prototype.fun2 = function(){};//Super.prototype.fun3…function Sub(){ Super.call(this); // 核心 // …}Sub.prototype = new Super(); 优点:不存在引用属性共享问题,可传参,函数可复用缺点:父类的属性会被实例化两次,获取不到真正实例父类(无法区分实例是父类创建还是父类创建的)优化: function Super(b){ this.b = b; this.fun = function(){} } Super.prototype.c = function(){console.log(1111)} function Foo(a,b){ this.a = a; Super.call(this,b); } Foo.prototype = Super.prototype; //修复构造函数: var foo1 = new Foo(1,2);缺点:无法区分实例是父类创建还是子类创建的3.3.4 寄生组合继承 function Super(b){ this.b = b; } Super.prototype.c = function(){console.log(1111)} function Foo(a,b){ this.a = a; Super.call(this,b); } var f = new Function(); f.prototype = Super.prototype; Foo.prototype = new f(); //等同于 Foo.prototype = Object.create(Super.prototype); var foo1 = new Foo(1,2);对父类的prototype进行一次寄生,即包装成一个空对象的prototype,再把这个对象实例化出来作为子类的peototype。缺点:无法区分实例是父类创建还是子类创建的可以添加以下代码:Foo.prototype.constructor = Foo这种解决方法不能用于上面的组合优化方法,因为子类父类引用的是同一个原型对象,修改会同时修改。总结:继承主要是实现子类对父类方法,属性的复用。来自原型对象的引用属性是所有实例共享的,所以我们要避免从原型中继承属性。在构造函数中通过call函数可以继承父类构造函数的属性和方法,但是通过这种方式实例化出来的实例会将父类方法多次存储,影响性能。通过组合继承我们使用call继承属性,使用原型继承方法,可以解决以上两个问题,但是通过这种方式实例化出来的对象会存储两份父类构造函数中的属性。用父类的原型构造一个新对象作为子类的原型,就解决了多次存储的问题,所以最终的寄生组合继承就是最佳继承方式,它的缺点就是书写起来比较麻烦。3.3.6 node源码中的继承实现function inherits(ctor, superCtor) { ctor.super_ = superCtor; ctor.prototype = Object.create(superCtor.prototype, { constructor: { value: ctor, enumerable: false, writable: true, configurable: true } });}; function Stream(){ //…}function OutgoingMessage() { Stream.call(this); //…}inherits(OutgoingMessage, Stream);OutgoingMessage.prototype.setTimeout = …以上是寄生组合继承的一个实例。1.在OutgoingMessage构造函数中通过call继承Stream构造中的属性。2.调用inherits方法继承Stream原型中的属性。3.扩展OutgoingMessage自身原型的函数。inherits方法中使用了Object.create方法,该方法的作用是通过指定的原型对象和属性创建一个新的对象。ctor.prototype=Object.create(superCtor.prototype,{…..});该方法实际上就做了我们上面寄生组合继承中的工作var f = new Function();f.prototype =superCtor.prototype;return new f();后面的参数是给原型对象添加属性,可选属性(非必填),即把自身作为新创建对象的构造函数。value: 表示constructor 的属性值;writable: 表示constructor 的属性值是否可写;[默认为: false]enumerable: 表示属性constructor 是否可以被枚举;[默认为: false]configurable: 表示属性constructor 是否可以被配置,例如 对obj.a做 delete操作是否允许;[默认为: false]3.4 es6继承的实现方式参考我这篇文章:https://segmentfault.com/a/11…3.5 描述new一个对象的过程创建一个对象{}.proto = 构造函数.prototypethis指向这个对象执行代码即对this赋值返回this3.6 zepto及其他源码中如何使用原型链var Zepto = (function(){ var $,zepto = {} // …省略N行代码… $ = function(selector, context){ return zepto.init(selector, context) } zepto.init = function(selector, context) { var dom // 针对参数情况,分别对dom赋值 // 最终调用 zepto.Z 返回的数据 return zepto.Z(dom, selector) } fnction Z(dom, selector) { var i, len = dom ? dom.length : 0 for (i = 0; i < len; i++) this[i] = dom[i] this.length = len this.selector = selector || ’’ } zepto.Z = function(dom, selector) { return new Z(dom, selector) } $.fn = { // 里面有若干个工具函数 } zepto.Z.prototype = Z.prototype = $.fn // …省略N行代码… return $})()window.Zepto = Zeptowindow.$ === undefined && (window.$ = Zepto) ...

January 8, 2019 · 3 min · jiezi

JavaScript 进阶知识 - 高级篇

JS高级前言经过前面几篇文章的学习,相信大家已经对js有了大部分的理解了,但是要想真正的掌握好js,本篇才是关键。由于js高级阶段的知识点比较难理解,所以本篇文章花了大量的时间去理思路,有可能有一些知识点遗漏了,也有肯能有部分知识点写的不对,欢迎大家留言纠正。另外,大家在以后的学习中千万不要被一些难点所吓到,听说有些知识点很难,其实并不是真正的难,只要你静下心慢慢的理解,其实还是很简单的。1.异常处理常见的异常分类运行环境的多样性导致的异常(浏览器)语法错误,代码错误异常最大的特征,就是一旦代码出现异常,后面的代码就不会执行。1.1异常捕获捕获异常,使用try-catch语句:try{ // 这里写可能出现异常的代码}catch(e){ // e-捕获的异常对象 // 可以在此处书写出现异常后的处理代码}异常捕获语句执行的过程为:代码正常运行, 如果在try中出现了错误,try里面出现错误的语句后面的代码都不再执行, 直接跳转到catch中catch中处理错误信息然后继续执行后面的代码如果try中没有出现错误, 那么不走catch直接执行后面的代码通过try-catch语句进行异常捕获之后,代码将会继续执行,而不会中断。示例代码:console.log(‘代码开始执行’);try{ console.log(num); // num 在外部是没有定义的}catch(e){ console.log(e); console.log(‘我已经把错误处理了’);}console.log(‘代码结束执行’);效果图:从效果图中我们可以看到,num是一个没有定义的变量,如果没有放在try-catch代码块中,后面的‘代码结束执行’就不会被打印。通过把try-catch放在代码块中,出现错误后,就不会影响后面代码的运行了,他会把错误信息打印出来。注意:语法错误异常用try-catch语句无法捕获,因为在预解析阶段,语法错误会直接检测出来,而不会等到运行的时候才报错。try-catch在一般日常开发中基本用不到,但是如果要写框架什么的,用的会非常多。因为这个会让框架变得健壮异常捕获语句的完整模式异常捕获语句的完整模式为try-catch-finallytry { //可能出现错误的代码} catch ( e ) { //如果出现错误就执行} finally { //结束 try 这个代码块之前执行, 即最后执行}finally中的代码,不管有没有发生异常,都会执行。一般用在后端语言中,用来释放资源,JavaScript中很少会用到1.2抛出异常如何手动的抛出异常呢?案例:自己写的一个函数,需要一个参数,如果用户不传参数,此时想直接给用户抛出异常,就需要了解如何抛出异常。抛出异常使用throw关键字,语法如下:throw 异常对象;异常对象一般是用new Error(“异常消息”), 也可以使用任意对象示例代码:function test(para){ if(para == undefined){ throw new Error(“请传递参数”); //这里也可以使用自定义的对象 throw {“id”:1, msg:“参数未传递”}; }}try{ test();}catch(e){ console.log(e);}效果图:1.3异常的传递机制function f1 () { f2(); }function f2 () { f3();}function f3() { throw new Error( ’error’ );}f1(); // f1 称为调用者, 或主调函数, f2 称为被调用者, 或被调函数当在被调函数内发生异常的时候,异常会一级一级往上抛出。2.面向对象编程在了解面向对象编程之前,我们先来了解下什么是面向过程,什么是面向对象,他们之间的区别是什么。2.1 面向过程和面向对象的的对比举个例子:日常洗衣服1.面向过程的思维方式:面向过程编程:将解决问题的关注点放在解决问题的具体细节上,关注如何一步一步实现代码细节;step 1:收拾脏衣服step 2:打开洗衣机盖step 3:将脏衣服放进去step 4:设定洗衣程序step 5:开始洗衣服step 6:打开洗衣机盖子step 7:晒衣服2.面向对象的思维方式:面向对象编程:将解决问题的关注点放在解决问题所需的对象上,我们重点找对象;人(对象)洗衣机(对象)在面向对象的思维方式中:我们只关心要完成事情需要的对象,面向对象其实就是对面向过程的封装;示例代码:在页面上动态创建一个元素//面向过程//1-创建一个divvar div=document.createElement(‘div’);//2-div设置内容div.innerHTML=‘我是div’;//3-添加到页面中document.body.appendChild(div);//面向对象$(‘body’).append(’<div>我也是div</div>’);我们可以看出,jQ封装的其实就是对面向过程的封装。总结: 面向对象是一种解决问题的思路,一种编程思想。2.2 面向对象编程举例设置页面中的div和p的边框为'1px solid red'1、传统的处理办法// 1> 获取div标签var divs = document.getElementsByTagName( ‘div’ );// 2> 遍历获取到的div标签for(var i = 0; i < divs.length; i++) { //3> 获取到每一个div元素,设置div的样式 divs[i].style.border = “1px dotted black”;}// 4> 获取p标签var ps = document.getElementsByTagName(“p”);// 5> 遍历获取到的p标签for(var j = 0; j < ps.length; j++) { // 获取到每一个p元素 设置p标签的样式 ps[j].style.border = “1px dotted black”; }2、使用函数进行封装优化// 通过标签名字来获取页面中的元素 function tag(tagName) { return document.getElementsByTagName(tagName); }// 封装一个设置样式的函数 function setStyle(arr) { for(var i = 0; i < arr.length; i++) { // 获取到每一个div或者p元素 arr[i].style.border = “1px solid #abc”; } }var dvs = tag(“div”);var ps = tag(“p”);setStyle(dvs); setStyle(ps);3、使用面向对象的方式// 更好的做法:是将功能相近的代码放到一起 var obj = { // 命名空间 getEle: { tag: function (tagName) { return document.getElementsByTagName(tagName); }, id: function (idName) { return document.getElementById(idName); } // … }, setCss: { setStyle: function (arr) { for(var i = 0; i < arr.length; i++) { arr[i].style.border = “1px solid #abc”; } }, css: function() {}, addClass: function() {}, removeClass: function() {} // … } // 属性操作模块 // 动画模块 // 事件模块 // … };var divs = obj.getEle.tag(‘div’);obj.setCss.setStyle(divs);2.3 面向对象的三大特性面向对象的三大特性分别是:‘封装’,‘继承’,‘多态’。1、封装性对象就是对属性和方法的封装,要实现一个功能,对外暴露一些接口,调用者只需通过接口调用即可,不需要关注接口内部实现原理。js对象就是“键值对”的集合键值如果是数据( 基本数据, 复合数据, 空数据 ), 就称为属性如果键值是函数, 那么就称为方法对象就是将属性与方法封装起来方法是将过程封装起来2、继承性所谓继承就是自己没有, 别人有,拿过来为自己所用, 并成为自己的东西2.1、传统继承基于模板子类可以使用从父类继承的属性和方法。class Person { string name; int age;}class Student : Person {}var stu = new Student();stu.name即:让某个类型的对象获得另一个类型的对象的属性的方法2.2、js 继承基于对象在JavaScript中,继承就是当前对象可以使用其他对象的方法和属性。js继承实现举例:混入(mix)// 参数o1和o2是两个对象,其中o1对象继承了所有o2对象的“k”属性或者方法var o1 = {};var o2 = { name: ‘Levi’, age: 18, gender: ‘male’};function mix ( o1, o2 ) { for ( var k in o2 ) { o1[ k ] = o2[ k ]; }}mix(o1, o2);console.log(o1.name); // “Levi"3、多态性(基于强类型,js中没有多态)只做了解同一个类型的变量可以表现出不同形态,用父类的变量指向子类的对象。动物 animal = new 子类(); // 子类:麻雀、狗、猫、猪、狐狸…动物 animal = new 狗();animal.叫();2.4 创建对象的方式1、字面量 {}var student1 = { name:‘诸葛亮’, score:100, code:1,}var student2 = { name:‘蔡文姬’, score:98, code:2,}var student3 = { name:‘张飞’, score:68, code:3,}字面量创建方式,代码复用性太低,每一次都需要重新创建一个对象。2、Object()构造函数var student1 = new Object(); student1.name = ‘诸葛亮’; student1.score = 100; student1.code = 1;var student2 = new Object(); student2.name = ‘蔡文姬’; student2.score = 98; student2.code = 2; var student3 = new Object(); student3.name = ‘张飞’; student3.score = 68; student3.code = 3;代码复用性太低,字面量创建的方式其实就是代替Object()构造函数创建方式的。3、自定义构造函数自定义构造函数,可以快速创建多个对象,并且代码复用性高。// 一般为了区分构造函数与普通函数,构造函数名首字母大写function Student(name,score,code){ this.name = name; this.score = score; this.code = code;}var stu1 = new Student(‘诸葛亮’,100,1);var stu2 = new Student(‘蔡文姬’,98,2);var stu3 = new Student(‘张飞’,68,3);构造函数语法:构造函数名首字母大写;构造函数一般与关键字:new一起使用;构造函数一般不需要设置return语句,默认返回的是新创建的对象;this指向的是新创建的对象。构造函数的执行过程:new关键字,创建一个新的对象,会在内存中开辟一个新的储存空间;让构造函数中的this指向新创建的对象;执行构造函数,给新创建的对象进行初始化(赋值);构造函数执行(初始化)完成,会将新创建的对象返回。构造函数的注意点:构造函数本身也是函数;构造函数有返回值,默认返回的是新创建的对象;但是如果手动添加返回值,添加的是值类型数据的时候,构造函数没有影响。如果添加的是引用类型(数组、对象等)值的时候,会替换掉新创建的对象。function Dog(){ this.name=“哈士奇”; this.age=0.5; this.watch=function(){ console.log(‘汪汪汪,禁止入内’); } // return false; 返回值不会改变,还是新创建的对象 // return 123; 返回值不会改变,还是新创建的对象 // return [1,2,3,4,5]; 返回值发生改变,返回的是这个数组 return {aaa:‘bbbb’}; // 返回值发生改变,返回的是这个对象}var d1=new Dog(); // 新创建一个对象console.log(d1);构造函数可以当做普通函数执行,里面的this指向的是全局对象window。 function Dog(){ this.name=“husky”; this.age=0.5; this.watch=function(){ console.log(‘汪汪汪,禁止入内’); } console.log(this); // window对象 return 1;}console.log(Dog()); // 打印 12.5 面向对象案例通过一个案例,我们来了解下面向对象编程(案例中有一个prototype概念,可以学完原型那一章后再来看这个案例)。需求:实现一个MP3音乐管理案例;同种类型的MP3,厂家会生产出成百上千个,但是每个MP3都有各自的样式、使用者、歌曲;每个MP3都有一样的播放、暂停、增删歌曲的功能(方法);图解:示例代码: // 每个MP3都有自己的 主人:owner 样式:color 歌曲:list function MP3(name,color,list){ this.owner = name || ‘Levi’; // 不传值时默认使用者是‘Levi’ this.color = color || ‘pink’; this.musicList = list || [ {songName:‘男人哭吧不是罪’,singer:‘刘德华’}, {songName:‘吻别’,singer:‘张学友’}, {songName:‘对你爱不完’,singer:‘郭富城’}, {songName:‘今夜你会不会来’,singer:‘黎明’} ]; } // 所有的MP3都有 播放 暂停 音乐 增删改查的功能 MP3.prototype = { // 新增 add:function(songName,singer){ this.musicList.push({songName:songName,singer:singer}); }, // 查找 select:function(songName){ for(var i=0;i<this.musicList.length;i++){ if(this.musicList[i].songName == songName){ return this.musicList[i]; } } return null; // 如果没有搜索到返回null }, // 修改 update:function(songName,singer){ // 先找到这首歌 在修改 var result = this.select(songName); // 查找 if(result){ result.singer = singer; // 修改 } }, // 删除 delete:function(songName){ // 先找到音乐 splice(index,1) var result = this.select(songName); // 知道该音乐的索引值 // 删除 if(result){ var index = this.musicList.indexOf(result); this.musicList.splice(index,1); // 从指定索引值来删除数据 } }, // 显示 show:function(){ console.log(this.owner+‘的MP3’); for(var i=0;i<this.musicList.length;i++){ console.log(this.musicList[i].songName +’—’+this.musicList[i].singer); } } } var XiaoHong = new MP3(‘小红’); // 实例小红MP3 var XiaoMing = new MP3(‘小明’); // 实例小明MP3 var XiaoDong = new MP3(‘小东’); // 实例小东MP3 XiaoHong.add(‘十年’,‘陈奕迅’); // 小红的歌单里添加歌曲 XiaoDong.add(‘月亮之上’,‘凤凰传奇’); // 小东的歌单里添加歌曲 XiaoMing.musicList = [ // 小明的歌单替换 { songName:‘精忠报国’, singer:‘屠洪刚’ }, { songName:‘窗外’, singer:‘未知’ } ]; // 展示各自的歌单 XiaoHong.show(); XiaoMing.show(); XiaoDong.show();打印结果:3.原型3.1 传统构造函数存在问题通过自定义构造函数的方式,创建小狗对象:两个实例化出来的“小狗”,它们都用的同一个say方法,为什么最后是false呢?function Dog(name, age) { this.name = name; this.age = age; this.say = function() { console.log(‘汪汪汪’); }}var dog1 = new Dog(‘哈士奇’, 1.5);var dog2 = new Dog(‘大黄狗’, 0.5);console.log(dog1);console.log(dog2);console.log(dog1.say == dog2.say); //输出结果为false画个图理解下:每次创建一个对象的时候,都会开辟一个新的空间,我们从上图可以看出,每只创建的小狗有一个say方法,这个方法都是独立的,但是功能完全相同。随着创建小狗的数量增多,造成内存的浪费就更多,这就是我们需要解决的问题。为了避免内存的浪费,我们想要的其实是下图的效果:解决方法:这里最好的办法就是将函数体放在构造函数之外,在构造函数中只需要引用该函数即可。function sayFn() { console.log(‘汪汪汪’);}function Dog(name, age) { this.name = name; this.age = age; this.say = sayFn();}var dog1 = new Dog(‘哈士奇’, 1.5);var dog2 = new Dog(‘大黄狗’, 0.5);console.log(dog1);console.log(dog2);console.log(dog1.say == dog2.say); //输出结果为 true这样写依然存在问题:全局变量增多,会增加引入框架命名冲突的风险代码结构混乱,会变得难以维护想要解决上面的问题就需要用到构造函数的原型概念。3.2 原型的概念prototype:原型。每个构造函数在创建出来的时候系统会自动给这个构造函数创建并且关联一个空的对象。这个空的对象,就叫做原型。关键点:每一个由构造函数创建出来的对象,都会默认的和构造函数的原型关联;当使用一个方法进行属性或者方法访问的时候,会先在当前对象内查找该属性和方法,如果当前对象内未找到,就会去跟它关联的原型对象内进行查找;也就是说,在原型中定义的方法跟属性,会被这个构造函数创建出来的对象所共享;访问原型的方式:构造函数名.prototype。示例图:示例代码: 给构造函数的原型添加方法function Dog(name,age){ this.name = name; this.age = age;}// 给构造函数的原型 添加say方法Dog.prototype.say = function(){ console.log(‘汪汪汪’);}var dog1 = new Dog(‘哈士奇’, 1.5);var dog2 = new Dog(‘大黄狗’, 0.5);dog1.say(); // 汪汪汪dog2.say(); // 汪汪汪我们可以看到,本身Dog这个构造函数中是没有say这个方法的,我们通过Dog.prototype.say的方式,在构造函数Dog的原型中创建了一个方法,实例化出来的dog1、dog2会先在自己的对象先找say方法,找不到的时候,会去他们的原型对象中查找。如图所示:在构造函数的原型中可以存放所有对象共享的数据,这样可以避免多次创建对象浪费内存空间的问题。3.3 原型的使用1、使用对象的动态特性使用对象的动态属性,其实就是直接使用prototype为原型添加属性或者方法。function Person () {}Person.prototype.say = function () { console.log( ‘讲了一句话’ );};Person.prototype.age = 18;var p = new Person();p.say(); // 讲了一句话console.log(p.age); // 182、直接替换原型对象每次构造函数创建出来的时候,都会关联一个空对象,我们可以用一个对象替换掉这个空对象。function Person () {}Person.prototype = { say : function () { console.log( ‘讲了一句话’ ); },};var p = new Person();p.say(); // 讲了一句话注意:使用原型的时候,有几个注意点需要注意一下,我们通过几个案例来了解一下。使用对象.属性名去获取对象属性的时候,会先在自身中进行查找,如果没有,就去原型中查找;// 创建一个英雄的构造函数 它有自己的 name 和 age 属性function Hero(){ this.name=“德玛西亚之力”; this.age=18;}// 给这个构造函数的原型对象添加方法和属性Hero.prototype.age= 30;Hero.prototype.say=function(){ console.log(‘人在塔在!!!’);}var h1 = new Hero();h1.say(); // 先去自身中找 say 方法,没有再去原型中查找 打印:‘人在塔在!!!‘console.log(p1.name); // “德玛西亚之力"console.log(p1.age); // 18 先去自身中找 age 属性,有的话就不去原型中找了使用对象.属性名去设置对象属性的时候,只会在自身进行查找,如果有,就修改,如果没有,就添加;// 创建一个英雄的构造函数function Hero(){ this.name=“德玛西亚之力”;}// 给这个构造函数的原型对象添加方法和属性Hero.prototype.age = 18;var h1 = new Hero();console.log(h1); // {name:“德玛西亚之力”}console.log(h1.age); // 18h1.age = 30; // 设置的时候只会在自身中操作,如果有,就修改,如果没有,就添加 不会去原型中操作console.log(h1); // {name:“德玛西亚之力”,age:30}console.log(h1.age); // 30一般情况下,不会将属性放在原型中,只会将方法放在原型中;在替换原型的时候,替换之前创建的对象,和替换之后创建的对象的原型不一致!!!// 创建一个英雄的构造函数 它有自己的 name 属性function Hero(){ this.name=“德玛西亚之力”;}// 给这个构造函数的默认原型对象添加 say 方法Hero.prototype.say = function(){ console.log(‘人在塔在!!!’);}var h1 = new Hero();console.log(h1); // {name:“德玛西亚之力”}h1.say(); // ‘人在塔在!!!’// 开辟一个命名空间 obj,里面有个 kill 方法var obj = { kill : function(){ console.log(‘大宝剑’); }}// 将创建的 obj 对象替换原本的原型对象Hero.prototype = obj;var h2 = new Hero();h1.say(); // ‘人在塔在!!!‘h2.say(); // 报错h1.kill(); // 报错h2.kill(); // ‘大宝剑’画个图理解下:图中可以看出,实例出来的h1对象指向的原型中,只有say()方法,并没有kill()方法,所以h1.kill()会报错。同理,h2.say()也会报错。3.4 __proto__属性在js中以_开头的属性名为js的私有属性,以__开头的属性名为非标准属性。__proto__是一个非标准属性,最早由firefox提出来。1、构造函数的 prototype 属性之前我们访问构造函数原型对象的时候,使用的是prototype属性:function Person(){}//通过构造函数的原型属性prototype可以直接访问原型Person.prototype;在之前我们是无法通过构造函数new出来的对象访问原型的:function Person(){}var p = new Person();//以前不能直接通过p来访问原型对象2、实例对象的 proto 属性__proto__属性最早是火狐浏览器引入的,用以通过实例对象来访问原型,这个属性在早期是非标准的属性,有了__proto__属性,就可以通过构造函数创建出来的对象直接访问原型。function Person(){}var p = new Person();//实例对象的__proto__属性可以方便的访问到原型对象p.proto;//既然使用构造函数的prototype和实例对象的__proto__属性都可以访问原型对象//就有如下结论p.proto === Person.prototype;如图所示:3、__proto__属性的用途可以用来访问原型;在实际开发中除非有特殊的需求,不要轻易的使用实例对象的__proto__属性去修改原型的属性或方法;在调试过程中,可以轻易的查看原型的成员;由于兼容性问题,不推荐使用。3.5 constuctor属性constructor:构造函数,原型的constructor属性指向的是和原型关联的构造函数。示例代码:function Dog(){ this.name=“husky”;}var d=new Dog();// 获取构造函数console.log(Dog.prototype.constructor); // 打印构造函数 Dogconsole.log(d.proto.constructor); // 打印构造函数 Dog如图所示:获取复杂类型的数据类型:通过obj.constructor.name的方式,获取当前对象obj的数据类型。在一个的函数中,有个返回值name,它表示的是当前函数的函数名;function Teacher(name,age){ this.name = name; this.age = age;}var teacher = new Teacher();// 假使我们只知道一个对象teacher,如何获取它的类型呢?console.log(teacher.proto.constructor.name); // Teacherconsole.log(teacher.constructor.name); // Teacher实例化出来的teacher对象,它的数据类型是啥呢?我们可以通过实例对象teacher.proto,访问到它的原型对象,再通过.constructor访问它的构造函数,通过.name获取当前函数的函数名,所以就能得到当前对象的数据类型。又因为.__proto__是一个非标准的属性,而且实例出的对象继承原型对象的方法,所以直接可以写成:obj.constructor.name。3.6 原型继承原型继承:每一个构造函数都有prototype原型属性,通过构造函数创建出来的对象都继承自该原型属性。所以可以通过更改构造函数的原型属性来实现继承。继承的方式有多种,可以一个对象继承另一个对象,也可以通过原型继承的方式进行继承。1、简单混入继承直接遍历一个对象,将所有的属性和方法加到另一对象上。var animal = { name:“Animal”, sex:“male”, age:5, bark:function(){ console.log(“Animal bark”); }};var dog = {};for (var k in animal){ dog[k]= animal[k];}console.log(dog); // 打印的对象与animal一模一样缺点:只能一个对象继承自另一个对象,代码复用太低了。2、混入式原型继承混入式原型继承其实与上面的方法类似,只不过是将遍历的对象添加到构造函数的原型上。var obj={ name:‘zs’, age:19, sex:‘male’ }function Person(){ this.weight=50;}for(var k in obj){ // 将obj里面的所有属性添加到 构造函数 Person 的原型中 Person.prototype[k] = obj[k];}var p1=new Person();var p2=new Person();var p3=new Person();console.log(p1.name); // ‘zs’console.log(p2.age); // 19console.log(p3.sex); // ‘male’面向对象思想封装一个原型继承我们可以利用面向对象的思想,将面向过程进行封装。function Dog(){ this.type = ‘yellow Dog’;}// 给构造函数 Dog 添加一个方法 extendDog.prototype.extend = function(obj){ // 使用混入式原型继承,给 Dog 构造函数的原型继承 obj 的属性和方法 for (var k in obj){ this[k]=obj[k]; }}// 调用 extend 方法Dog.prototype.extend({ name:“二哈”, age:“1.5”, sex:“公”, bark:function(){ console.log(‘汪汪汪’); }});3、替换式原型继承替换式原型继承,在上面已经举过例子了,其实就是将一个构造函数的原型对象替换成另一个对象。function Person(){ this.weight=50;}var obj={ name:‘zs’, age:19, sex:‘male’}// 将一个构造函数的原型对象替换成另一个对象Person.prototype = obj;var p1=new Person();var p2=new Person();var p3=new Person();console.log(p1.name); // ‘zs’console.log(p2.age); // 19console.log(p3.sex); // ‘male’之前我们就说过,这样做会产生一个问题,就是替换的对象会重新开辟一个新的空间。替换式原型继承时的bug替换原型对象的方式会导致原型的constructor的丢失,constructor属性是默认原型对象指向构造函数的,就算是替换了默认原型对象,这个属性依旧是默认原型对象指向构造函数的,所以新的原型对象是没有这个属性的。解决方法:手动关联一个constructor属性function Person() { this.weight = 50;}var obj = { name: ‘zs’, age: 19, sex: ‘male’}// 在替换原型对象函数之前 给需要替换的对象添加一个 constructor 属性 指向原本的构造函数obj.constructor = Person;// 将一个构造函数的原型对象替换成另一个对象Person.prototype = obj;var p1 = new Person();console.log(p1.proto.constructor === Person); // true4、Object.create()方法实现原型继承当我们想把对象1作为对象2的原型的时候,就可以实现对象2继承对象1。前面我们了解了一个属性:proto,实例出来的对象可以通过这个属性访问到它的原型,但是这个属性只适合开发调试时使用,并不能直接去替换原型对象。所以这里介绍一个新的方法:Object.create()。语法: var obj1 = Object.create(原型对象);示例代码: 让空对象obj1继承对象obj的属性和方法var obj = { name : ‘盖伦’, age : 25, skill : function(){ console.log(‘大宝剑’); }}// 这个方法会帮我们创建一个原型是 obj 的对象var obj1 = Object.create(obj);console.log(obj1.name); // “盖伦"obj1.skill(); // “大宝剑"兼容性:由于这个属性是ECMAScript5的时候提出来的,所以存在兼容性问题。利用浏览器的能力检测,如果存在Object.create则使用,如果不存在的话,就创建构造函数来实现原型继承。// 封装一个能力检测函数function create(obj){ // 判断,如果浏览器有 Object.create 方法的时候 if(Object.create){ return Object.create(obj); }else{ // 创建构造函数 Fun function Fun(){}; Fun.prototype = obj; return new Fun(); }}var hero = { name: ‘盖伦’, age: 25, skill: function () { console.log(‘大宝剑’); }}var hero1 = create(hero);console.log(hero1.name); // “盖伦"console.log(hero1.proto == hero); // true4.原型链对象有原型,原型本身又是一个对象,所以原型也有原型,这样就会形成一个链式结构的原型链。4.1 什么是原型链示例代码: 原型继承练习// 创建一个 Animal 构造函数function Animal() { this.weight = 50; this.eat = function() { console.log(‘蜂蜜蜂蜜’); }}// 实例化一个 animal 对象var animal = new Animal();// 创建一个 Preson 构造函数function Person() { this.name = ‘zs’; this.tool = function() { console.log(‘菜刀’); }}// 让 Person 继承 animal (替换原型对象)Person.prototype = animal;// 实例化一个 p 对象 var p = new Person();// 创建一个 Student 构造函数function Student() { this.score = 100; this.clickCode = function() { console.log(‘啪啪啪’); }}// 让 Student 继承 p (替换原型对象)Student.prototype = p;//实例化一个 student 对象var student = new Student();console.log(student); // 打印 {score:100,clickCode:fn}// 因为是一级级继承下来的 所以最上层的 Animate 里的属性也是被继承的console.log(student.weight); // 50student.eat(); // 蜂蜜蜂蜜student.tool(); // 菜刀如图所示:我们将上面的案例通过画图的方式展现出来后就一目了然了,实例对象animal直接替换了构造函数Person的原型,以此类推,这样就会形成一个链式结构的原型链。完整的原型链结合上图,我们发现,最初的构造函数Animal创建的同时,会创建出一个原型,此时的原型是一个空的对象。结合原型链的概念:“原型本身又是一个对象,所以原型也有原型”,那么这个空对象往上还能找出它的原型或者构造函数吗?我们如何创建一个空对象? 1、字面量:{};2、构造函数:new Object()。我们可以简单的理解为,这个空的对象就是,构造函数Object的实例对象。所以,这个空对象往上面找是能找到它的原型和构造函数的。// 创建一个 Animal 构造函数function Animal() { this.weight = 50; this.eat = function() { console.log(‘蜂蜜蜂蜜’); }}// 实例化一个 animal 对象var animal = new Animal();console.log(animal.proto); // {}console.log(animal.proto.proto); // {}console.log(animal.proto.proto.constructor); // function Object(){}console.log(animal.proto.proto.proto); // null如图所示:4.2 原型链的拓展1、描述出数组“[]”的原型链结构// 创建一个数组var arr = new Array();// 我们可以看到这个数组是构造函数 Array 的实例对象,所以他的原型应该是:console.log(Array.prototype); // 打印出来还是一个空数组// 我们可以继续往上找 console.log(Array.prototype.proto); // 空对象// 继续console.log(Array.prototype.proto.proto) // null如图所示:2、扩展内置对象给js原有的内置对象,添加新的功能。注意:这里不能直接给内置对象的原型添加方法,因为在开发的时候,大家都会使用到这些内置对象,假如大家都是给内置对象的原型添加方法,就会出现问题。错误的做法:// 第一个开发人员给 Array 原型添加了一个 say 方法Array.prototype.say = function(){ console.log(‘哈哈哈’);}// 第二个开发人员也给 Array 原型添加了一个 say 方法Array.prototype.say = function(){ console.log(‘啪啪啪’);}var arr = new Array();arr.say(); // 打印 “啪啪啪” 前面写的会被覆盖为了避免出现这样的问题,只需自己定义一个构造函数,并且让这个构造函数继承数组的方法即可,再去添加新的方法。// 创建一个数组对象 这个数组对象继承了所有数组中的方法var arr = new Array();// 创建一个属于自己的构造函数function MyArray(){}// 只需要将自己创建的构造函数的原型替换成 数组对象,就能继承数组的所有方法MyArray.prototype = arr;// 现在可以单独的给自己创建的构造函数的原型添加自己的方法MyArray.prototype.say = function(){ console.log(‘这是我自己添加的say方法’);}var arr1 = new MyArray();arr1.push(1); // 创建的 arr1 对象可以使用数组的方法arr1.say(); // 也可以使用自己添加的方法 打印“这是我自己添加的say方法”console.log(arr1); // [1]4.3 属性的搜索原则当通过对象名.属性名获取属性时,会遵循以下属性搜索的原则:1-首先去对象自身属性中找,如果找到直接使用,2-如果没找到,去自己的原型中找,如果找到直接使用,3-如果没找到,去原型的原型中继续找,找到直接使用,4-如果没有会沿着原型不断向上查找,直到找到null为止。5.Object.prototype成员介绍我们可以看到所有的原型最终都会继承Object的原型:Object.prototype。打印看看Object的原型里面有什么:// Object的原型console.log(Object.prototype)如图所示:我们可以看到Object的原型里有很多方法,下面就来介绍下这些方法的作用。5.1 constructor 属性指向了和原型相关的构造函数5.2 hasOwnProperty 方法判断对象自身是否拥有某个属性,返回值:布尔类型。示例代码:function Hero() { this.name = ‘盖伦’; this.age = ‘25’; this.skill = function () { console.log(‘盖伦使用了大宝剑’); }}var hero = new Hero();console.log(hero.name); // ‘盖伦’hero.skill(); // ‘盖伦使用了大宝剑’console.log(hero.hasOwnProperty(“name”)); // trueconsole.log(hero.hasOwnProperty(“age”)); // trueconsole.log(hero.hasOwnProperty(“skill”)); // trueconsole.log(hero.hasOwnProperty(“toString”)); // false toString是在原型链当中的方法,并不是这里对象的方法console.log(’toString’ in hero); // true in方法 判断对象自身或者原型链中是否有某个属性5.3 isPrototypeOf 方法对象1.isPrototypeOf(对象2),判断对象1是否是对象2的原型,或者对象1是否是对象2原型链上的原型。示例代码:var obj = { age: 18}var obj1 = {};// 创建一个构造函数function Hero() { this.name = ‘盖伦’;}// 将这个构造函数的原型替换成 objHero.prototype = obj;// 实例化一个 hero 对象var hero = new Hero();console.log(obj.isPrototypeOf(hero)); // true 判断 obj 是否是 hero 的原型console.log(obj1.isPrototypeOf(hero)); // false 判断 obj1 是否是 hero 的原型console.log(Object.prototype.isPrototypeOf(hero)); // true 判断 Object.prototype 是否是 hero 的原型// 注意 这里的 Object.prototype 是原型链上最上层的原型对象5.4 propertyIsEnumerable 方法对象.propertyIsEnumerable(‘属性或方法名’),判断一个对象是否有该属性,并且这个属性可以被for-in遍历,返回值:布尔类型。示例代码:// 创建一个构造函数function Hero (){ this.name = ‘盖伦’; this.age = 25; this.skill = function(){ console.log(‘盖伦使用了大宝剑’); }}// 创建一个对象var hero = new Hero();// for-in 遍历这个对象 我们可以看到分别打印了哪些属性和方法for(var k in hero){ console.log(k + ‘—’ + hero[k]); // “name-盖伦” “age-25” “skill-fn()”}// 判断一个对象是否有该属性,并且这个属性可以被 for-in 遍历console.log(hero.propertyIsEnumerable(’name’)); // trueconsole.log(hero.propertyIsEnumerable(‘age’)); // trueconsole.log(hero.propertyIsEnumerable(’test’)); // false5.5 toString 和 toLocalString 方法两种方法都是将对象转成字符串的,只不过toLocalString是按照本地格式进行转换。示例代码:// 举个例子,时间的格式可以分为世界时间的格式和电脑本地的时间格式var date = new Date();// 直接将创建的时间对象转换成字符串console.log(date.toString());// 将创建的时间对象按照本地格式进行转换console.log(date.toLocaleString());效果图:5.6 valueOf 方法返回指定对象的原始值。MDN官方文档6.静态方法和实例方法静态方法和实例方法这两个概念其实也是从面相对象的编程语言中引入的,对应到JavaScript中的理解为:静态方法: 由构造函数调用的在js中,我们知道有个Math构造函数,他有一个Math.abs()的方法,这个方法由构造函数调用,所以就是静态方法。Math.abs();实例方法: 由构造函数创建出来的对象调用的var arr = new Array();// 由构造函数 Array 实例化出来的对象 arr 调用的 push 方法,叫做实例方法arr.push(1);示例代码:function Hero(){ this.name=‘亚索’; this.say=function(){ console.log(‘哈撒ki’); }}Hero.prototype.skill=function(){ console.log(‘吹风’);}// 直接给构造函数添加一个 run 方法(函数也是对象,可以直接给它加个方法)Hero.run=function(){ console.log(‘死亡如风,常伴吾身’);}var hero = new Hero();hero.say();hero.skill(); //实例方法Hero.run(); //静态方法如果这个方法是对象所有的,用实例方法。一般的工具函数,用静态方法,直接给构造函数添加方法,不需要实例化,通过构造函数名直接使用即可;7.作用域“域”,表示的是一个范围,“作用域”就是作用范围。作用域说明的是一个变量可以在什么地方被使用,什么地方不能被使用。7.1 块级作用域在ES5及ES5之前,js中是没有块级作用域的。{ var num = 123; { console.log( num ); // 123 }}console.log( num ); // 123上面这段代码在JavaScript中是不会报错的,但是在其他的编程语言中(C#、C、JAVA)会报错。这是因为,在JavaScript中没有块级作用域,使用{}标记出来的代码块中声明的变量num,是可以被{}外面访问到的。但是在其他的编程语言中,有块级作用域,那么{}中声明的变量num,是不能在代码块外部访问的,所以报错。注意:会计作用域只在在ES5及ES5之前不起作用,但是在ES6开始,js中是存在块级作用域的。7.2 词法作用域词法( 代码 )作用域,就是代码在编写过程中体现出来的作用范围。代码一旦写好,不用执行,作用范围就已经确定好了,这个就是所谓词法作用域。在js中词法作用域规则:函数允许访问函数外的数据;整个代码结构中只有函数可以限定作用域;作用域规则首先使用提升规则分析;如果当前作用规则中有名字了,就不考虑外面的名字。作用域练习:第一题var num=250;function test(){ // 会现在函数内部查找有没有这个num变量,有的话调用,没有的话会去全局中查找,有就返回,没有就返回undefined console.log(num); // 打印 250}function test1(){ var num=222; test();}test1(); 第二题if(false){ var num = 123;}console.log(num); // undefined // {}是没有作用域的 但是有判断条件,var num会提升到判断语句外部 所以不会报错 打印的是undefined第三题var num = 123;function foo() { var num = 456; function func() { console.log( num ); } func();}foo(); // 456// 调用foo时,在函数内部调用了func,打印num的时候,会先在func中查找num 没有的时候会去外层作用域找,找到即返回,找不到即再往上找。第四题var num1 = 123;function foo1() { var num1 = 456; function foo2() { num1 = 789; function foo3 () { console.log( num1 ); // 789 自己的函数作用域中没有就一层层往上找 } foo3(); } foo2();}foo1();console.log( num1 ); // 123 7.3 变量提升(预解析)JavaScript是解释型的语言,但是它并不是真的在运行的时候逐句的往下解析执行。我们来看下面这个例子:func();function func(){ alert(“函数被调用了”);}在上面这段代码中,函数func的调用是在其声明之前,如果说JavaScript代码真的是逐句的解析执行,那么在第一句调用的时候就会出错,然而事实并非如此,上面的代码可以正常执行,并且alert出来"函数被调用了”。所以,可以得出结论,JavaScript并非仅在运行时简简单单的逐句解析执行!JavaScript预解析JavaScript引擎在对JavaScript代码进行解释执行之前,会对JavaScript代码进行预解析,在预解析阶段,会将以关键字var和function开头的语句块提前进行处理。关键问题是怎么处理呢?当变量和函数的声明处在作用域比较靠后的位置的时候,变量和函数的声明会被提升到当前作用域的开头。示例代码:函数名提升正常函数书写方式function func(){ alert(“函数被调用了”);}func();预解析之后,函数名提升func();function func(){ alert(“函数被调用了”);}示例代码:变量名提升正常变量书写方式alert(a); // undefined var a = 123;// 由于JavaScript的预解析机制,上面这段代码,alert出来的值是undefined,// 如果没有预解析,代码应该会直接报错a is not defined,而不是输出值。不是说要提前的吗?那不是应该alert出来123,为什么是undefined?// 变量的时候 提升的只是变量声明的提升,并不包括赋值var a; // 这里是声明alert(a); // 变量声明之后并未有初始化和赋值操作,所以这里是 undefineda = 123; // 这里是赋值注意:特殊情况1、函数不能被提升的情况函数表达式创建的函数不会提升test(); // 报错 “test is not a function"var test = function(){ console.log(123);}new Function创建的函数也不会被提升test(); // 报错 “test is not a function"var test = new Function(){ console.log(123);}2、出现同名函数test(); // 打印 ‘好走的都是下坡路’// 两个函数重名,这两个函数都会被提升,但是后面的函数会覆盖掉前面的函数function test(){ console.log(‘众里寻她千百度,他正在自助烤肉….’);}function test(){ console.log(‘好走的都是下坡路’);}3、函数名与变量名同名// 如果函数和变量重名,只会提升函数,变量不会被提升console.log(test); // 打印这个test函数function test(){ console.log(‘我是test’);}var test=200;再看一种情况:var num = 1;function num () { console.log(num); // 报错 “num is not a function”}num();直接上预解析后的代码:function num(){ console.log(num);}num = 1;num();4、条件式的函数声明// 如果是条件式的函数申明, 这个函数不会被预解析test(); // test is not a functionif(true){ function test(){ console.log(‘只是在人群中多看了我一眼,再也忘不掉我容颜…’); }}预解析是分作用域的声明提升并不是将所有的声明都提升到window 对象下面,提升原则是提升到变量运行的当前作用域中去。示例代码:function showMsg(){ var msg = ‘This is message’;}alert(msg); // 报错“Uncaught ReferenceError: msg is not defined”预解析之后:function showMsg(){ var msg; // 因为函数本身就会产生一个作用域,所以变量声明在提升的时候,只会提升在当前作用域下最前面 msg = ‘This is message’;}alert(msg); // 报错“Uncaught ReferenceError: msg is not defined”预解析是分段的分段,其实就分script标签的<script>func(); // 输出 AA2;function func(){ console.log(‘AA1’);}function func(){ console.log(‘AA2’);}</script><script>function func(){ console.log(‘AA3’);}</script>在上面代码中,第一个script标签中的两个func进行了提升,第二个func覆盖了第一个func,但是第二个script标签中的func并没有覆盖上面的第二个func。所以说预解析是分段的。tip: 但是要注意,分段只是单纯的针对函数,变量并不会分段预解析。函数预解析的时候是分段的,但是执行的时候不分段<script> //变量预解析是分段的 ,但是函数的执行是不分段 var num1=100; // test3(); 报错,函数预解析的时候分段,执行的时候才不分段 function test1(){ console.log(‘我是test1’); } function test2(){ console.log(‘我是test2’); }</script><script> var num2=200; function test3(){ console.log(’test3’); } test1(); // 打印 ‘我是test1’ 函数执行的时候不分段 console.log(num1); // 100</script>7.4 作用域链什么是作用域链?只有函数可以制造作用域结构,那么只要是代码,就至少有一个作用域, 即全局作用域。凡是代码中有函数,那么这个函数就构成另一个作用域。如果函数中还有函数,那么在这个作用域中就又可以诞生一个作用域。将这样的所有的作用域列出来,可以有一个结构: 函数内指向函数外的链式结构。就称作作用域链。例如:function f1() { function f2() { }}var num = 456;function f3() { function f4() { }}示例代码:var num=200;function test(){ var num=100; function test1(){ var num=50; function test2(){ console.log(num); } test2(); } test1();}test(); // 打印 “50”如图所示:绘制作用域链的步骤:看整个全局是一条链, 即顶级链, 记为0级链看全局作用域中, 有什么变量和函数声明, 就以方格的形式绘制到0级练上再找函数, 只有函数可以限制作用域, 因此从函数中引入新链, 标记为1级链然后在每一个1级链中再次往复刚才的行为变量的访问规则:首先看变量在第几条链上, 在该链上看是否有变量的定义与赋值, 如果有直接使用如果没有到上一级链上找( n - 1 级链 ), 如果有直接用, 停止继续查找.如果还没有再次往上刚找… 直到全局链( 0 级 ), 还没有就是 is not defined注意,同级的链不可混合查找来点案例练练手第一题:function foo() { var num = 123; console.log(num); //123}foo();console.log(num); // 报错第二题:var scope = “global”;function foo() { console.log(scope); // undefined var scope = “local”; console.log(scope); // ’local’}foo();// 预解析之后// var scope = “global”;// function foo() {// var scope;// console.log(scope); // undefined// scope = “local”;// console.log(scope); // local// }第三题:if(“a” in window){ var a = 10;}console.log(a); // 10// 预解析之后// var a;// if(“a” in window){// a = 10; // 判断语句不产生作用域// }// console.log(a); // 10第四题:if(!“a” in window){ var a = 10;}console.log(a); // undefined// 预解析之后// var a;// if(!“a” in window){// a = 10; // 判断语句不产生作用域// }// console.log(a); // undefined第五题// console.log(num); 报错 虽然num是全局变量 但是不会提升function test(){ num = 100; }test();console.log(num); // 100第六题var foo = 1;function bar() { if(!foo) { var foo = 10; } console.log(foo); // 10}bar();// 预解析之后// var foo=1;// function bar(){// var foo;// if(!foo){// foo=10;// }// console.log(foo); // 10// }// bar();8.FunctionFunction是函数的构造函数,你可能会有点蒙圈,没错,在js中函数与普通的对象一样,也是一个对象类型,只不过函数是js中的“一等公民”。这里的Function类似于Array、Object等8.1 创建函数的几种方式1、函数字面量(直接声明函数)创建方式function test(){ // 函数体} // 类似于对象字面量创建方式:{}2、函数表达式var test = function(){ // 函数体}3、Function构造函数创建// 构造函数创建一个空的函数var fn = new Function();fn1(); // 调用函数函数扩展名有没有一种可能,函数表达式声明函数时,function 也跟着一个函数名,如:var fn = function fn1(){}? 答案是可以的,不过fn1只能在函数内部使用,并不能在外部调用。var fn = function fn1(a,b,c,d){ console.log(‘当前函数被调用了’); // 但是,fn1可以在函数的内部使用 console.log(fn1.name); console.log(fn1.length); // fn1(); 注意,这样调用会引起递归!!! 下面我们会讲到什么是递归。}// fn1(); // 报错,fn1是不能在函数外部调用的fn(); // “当前函数被调用了”// 函数内部使用时打印:// “当前函数被调用了”// console.log(fn1.name); => “fn1”// console.log(fn1.length); => 48.2 Function 构造函数创建函数上面我们知道了如何通过Function构造函数创建一个空的函数,这里我们对它的传参详细的说明下。1、不传参数时// 不传参数时,创建的是一个空的函数var fn1 = new Function();fn1(); // 调用函数2、只传一个参数// 只传一个参数的时候,这个参数就是函数体// 语法:var fn = new Function(函数体);var fn2 = new Function(‘console.log(2+5)’);f2(); // 73、传多个参数// 传多个参数的时候,最后一个参数为函数体,前面的参数都是函数的形参名// 语法:var fn = new Function(arg1,arg2,arg3…..argn,metthodBody);var fn3 = new Function(’num1’,’num2’,‘console.log(num1+num2)’);f3(5,2); // 78.3 Function 的使用1、用Function创建函数的方式封装一个计算m - n之间所有数字的和的函数//求 m-n之间所有数字的和//var sum=0;//for (var i = m; i <=n; i++) {// sum+=i;//}var fn = new Function(’m’,’n’,‘var sum=0;for (var i = m; i <=n; i++) {sum+=i;} console.log(sum);’);fn(1,100); // 5050函数体参数过长问题:函数体过长时,可读性很差,所以介绍解决方法:1)字符串拼接符“+”var fn = new Function( ’m’, ’n’, ‘var sum=0;’+ ‘for (var i = m; i <=n; i++) {’+ ‘sum += i;’+ ‘}’+ ‘console.log(sum);’ );fn(1,100); // 50502)ES6中新语法“ ”,(在esc键下面)表示可换行字符串的界定符,之前我们用的是单引号或者双引号来表示一个字符串字面量,在ES6中可以用反引号来表示该字符串可换行。new Function( 'm', 'n', var sum=0; for (var i = m; i <=n; i++) { sum+=i; } console.log(sum);`);3)模板方式<!– 新建一个模板 –><script type=“text/template” id=“tmp”> var sum=0; for (var i = m; i <=n; i++) { sum += i; } console.log(sum);</script><script> // 获取模板内的内容 var methodBody = document.querySelector(’#tmp’).innerHTML; console.log(methodBody); var fn = new Function(’m’,’n’,methodBody); fn(2,6); // 20</script>2、eval 函数eval函数可以直接将把字符串的内容,作为js代码执行,前提是字符串代码符合js代码规范。这里主要是用作跟Function传参比较。eval 和 Function 的区别:Function();中,方法体是字符串,必须调用这个函数才能执行eval(); 可以直接执行字符串中的js代码存在的问题:性能问题因为eval里面的代码是直接执行的,所以当在里面定义一个变量的时候,这个变量是不会预解析的,所以会影响性能。// eval 里面的代码可以直接执行,所以下面的打印的 num 可以访问到它// 但是这里定义的 num 是没有预解析的,所以变量名不会提升,从而性能可能会变慢eval(‘var num = 123;’);console.log(num); // 123安全问题主要的安全问题是可能会被利用做XSS攻击(跨站脚本攻击(Cross Site Scripting)),eval也存在一个安全问题,因为它可以执行传给它的任何字符串,所以永远不要传入字符串或者来历不明和不受信任源的参数。示例代码: 实现一个简单的计算器<!– html 部分 –><input type=“text” class=“num1”><select class=“operator”> <option value="+">+</option> <option value=”-">-</option> <option value=”"></option> <option value=”/">/</option></select><input type=“text” class=“num2”><button>=</button><input type=“text” class=“result”><!– js 部分 –><script> document.querySelector(‘button’).onclick=function(){ var num1 = document.querySelector(’.num1’).value; var num2 = document.querySelector(’.num2’).value; var operator = document.querySelector(’.operator’).value; // result其实最终获得的就是 num1 + operator + num2的字符串 但是他能够直接执行并计算 var result = eval(num1 + operator + num2); //计算 document.querySelector(’.result’).value = result; //显示 }</script>效果图:8.4 Function 的原型链结构在7.2章节中我们知道函数也还可以通过构造函数的方式创建出来,既然可以通过构造函数的方式创建,那么函数本身也是有原型对象的。示例代码:// 通过Function构造函数创建一个函数testvar test = new Function();// 既然是通过构造函数创建的,那么这个函数就有指向的原型console.log(test.proto); // 打印出来的原型是一个空的函数console.log(test.proto.proto); // 空的函数再往上找原型是一个空的对象console.log(test.proto.proto.proto); // 再往上找就是null了// 函数原型链: test() —> Function.prototype —> Object.prototype —> null如图所示:通过上图,可以直观的看出,函数也是有原型的。那一个完整的原型链究竟是什么样子的呢?下面我们一起做个总结。8.5 完整的原型链绘制完整原型链的步骤:1、先将一个对象的原型画出来2、再把对象的原型的原型链画出来 ,到null结束3、把对象的构造函数的原型链画出来4、把Function和Object的原型关系给画出来示例代码:// 创建一个构造函数function Person(){ this.name = ‘Levi’; this.age = 18;}// 实例化一个对象var p = new Person();如图所示:总结:Function构造函数的原型,在Object的原型链上;Object构造函数的原型,在Function的原型链上;9.arguments对象在每一个函数调用的过程中, 函数代码体内有一个默认的对象arguments, 它存储着实际传入的所有参数。示例代码:// 封装一个加法函数function add(num1,num2){ console.log(num1+num2);}add(1); // NaNadd(1,2); // 3add(1,2,3); // 3在调用函数时,实参和形参的个数可以不一样,但是没有意义。在函数内部有个arguments对象(注意:是在函数内部),arguments是一个伪数组对象。它表示在函数调用的过程中传入的所有参数(实参)的集合。在函数调用过程中不规定参数的个数与类型,可以使得函数调用变得非常灵活性。function add(num1,num2){ console.log(arguments); // 打印的是一个伪数组}add(1,2,3,4); length:表示的是实参的个数;callee:指向的就是arguments对象所在的函数;示例代码:封装一个求最大值的函数,因为不知道需要传进多少实参,所以直接用伪数组arguments获取调用的实参function max(){ // 假使实参的第一个数字最大 var maxNum = arguments[0]; // 循环这个伪数组 for(var i = 0; i < arguments.length; i++){ if(maxNUm < arguments[i]){ maxNUm = arguments[i]; } return maxNum; } }// 调用console.log(max(1,9,12,8,22,5)); // 2210. 函数的四种调用模式四种调用模式分别是:“函数调用模式”、“方法调用模式”、“构造器调用模式”、“上下文调用模式”。其实就是分析this是谁的问题。只看函数是怎么被调用的,而不管函数是怎么来的。分析this属于哪个函数;分析这个函数是以什么方式调用的;什么是函数? 什么是方法?如果一个函数是挂载到一个对象中,那么就把这个函数称为方法如果一个函数直接放在全局中,由Window对象调用,那么他就是一个函数。// 函数function fn() {}var f = function() {};fn();f();// 方法var obj = { say: function() {}};obj.say();fn和f都是函数,say是一个方法10.1 函数模式函数模式其实就是函数调用模式,this是指向全局对象window的。this -> window示例代码:// 函数调用模式:// 创建的全局变量相当于window的属性var num = 999;var fn = function () { console.log(this); // this 指向的是 window 对象 console.log(this.num); // 999};fn();10.2 方法模式方法模式其实就是方法调用模式,this是指向调用方法的对象。this -> 调用方法的对象示例代码:// this指向的是obj var age = 38;var obj = { age: 18, getAge: function () { console.log(this); // this指向的是对象obj {age:18,getAge:f()} console.log(this.age); // 18 } };obj.getAge(); // getAge() 是对象 obj 的一个方法10.3 构造器模式构造器模式其实就是构造函数调用模式,this指向新创建出来的实例对象。this -> 新创建出来的实例对象示例代码:// this指向的是实例化出来的对象function Person(name){ this.name = name; console.log(this);}var p1 = new Person(‘Levi’); // Person {name: “Levi”}var p2 = new Person(‘Ryan’); // Person {name: “Ryan”}构造函数的返回值:如果返回的是基本类型function Person() { return 1;}var p1 = new Person();console.log(p1); // 打印Person {}构造函数内有返回值,且是基本类型的时候,返回值会被忽略掉,返回的是实例出来的对象。如果返回的是引用类型function Person() { return { name: ’levi’, age: 18 };}var p1 = new Person();console.log(p1); // 此时打印 Object {name: ’levi’, age: 18}构造函数内的返回值是一个引用类型的时候,返回的就是这个指定的引用类型。10.4 上下文(借用方法)模式上下文,即环境,用于指定方法内部的this,上下文调用模式中,this可以被随意指定为任意对象。上下文模式有两种方法,是由函数调用的:函数名.apply( … );函数名.call( … );1、apply 方法语法:fn.apply(thisArg, array);参数:第一个参数:表示函数内部this的指向(或者:让哪个对象来借用这个方法)第二个参数:是一个数组(或者伪数组),数组中的每一项都将作为被调用方法的参数示例代码:// 没有参数function fn (){ console.log(this.name);}var obj = { name : ‘Levi丶’}// this 指向 obj,fn 借用obj方法里面的 name 属性fn.apply(obj); // 打印 ‘Levi丶’// 有参数function fn (num1, num2){ console.log(num1 + num2);}var obj = {}// this 指向 obj,数组中的数据是方法 fn 的参数fn.apply(obj, [1 , 2]); // 打印 3注意:apply方法的第一个参数,必须是一个对象!如果传入的参数不是一个对象,那么这个方法内部会将其转化为一个包装对象。function fn() { console.log(this);}fn.apply(1); // 包装对象fn.apply(‘abc’); // 包装对象fn.apply(true); // 包装对象指向window的几种方式:function fn(){ }fn.apply(window);fn.apply();fn.apply(null);fn.apply(undefined);具体应用:求数组中的最大数// 以前的方法,假设第一项最大,然后与后面每一项比较,得到最大的项var arr = [1, 3, 6, 10, 210, 23, 33, 777, 456];var maxNum = arr[0];for(var i = 1; i < arr.length; i++) { if(maxNum < arr[i]) { maxNum = arr[i]; }}console.log(maxNum); // 777// 利用 内置对象的 apply 的方法var arr = [1, 3, 6, 10, 210, 23, 33, 777, 456];// max 是内置对象 Math 求最大值的一个方法var maxNum = Math.max.apply(null, arr);console.log(maxNum); // 777将传进的参数每一项之间用“-”连接// 思考:参数个数是用户随机传的,没有具体的一个值,这时候就需要用到 arguments 的概念了function fn (){ // 数组原型中有一个join方法,他的接收的参数是一个字符串 // join.apply的第一个参数指向 arguments 对象,第二个参数是jion方法需要的参数 return Array.prototype.join.apply(arguments, [’-’]);}var ret = fn(‘a’, ‘b’, ‘c’, ’d’, ’e’);console.log(ret); // ‘a-b-c-d-e'2、call 方法call方法的作用于apply方法的作用相同,唯一不同的地方就是第二个参数不同。语法:fn.apply(thisArg, parm1,parm2,parm3,…);参数:第一个参数:表示函数内部this的指向(或者:让哪个对象来借用这个方法)第二个及后面的参数:不是之前数组的形式了,对应方法调用的每一个参数示例代码:function fn(num1, num2, num3) { console.log(num1, num2, num3);}var obj = {};fn.call(obj, [1, 3, 9], 0, 1); // [1, 3, 9] 0 1fn.call(obj, [1, 3, 9]); // [1, 3, 9] undefined undefined3、apply 和 call 的区别两者在功能上一模一样,唯一的区别就是第二个参数传递的类型不一样。什么时候用apply?什么时候用call呢?其实用哪个都可以,在参数少的情况下,我们可以使用call方法,但是如果参数是伪数组或者是数组的时候,call方法就不适用了,还需要将伪数组中的每一项取出来作为方法的参数,此时apply更加实用。10.5 面试题分析面试题1:var age = 38;var obj = { age: 18, getAge: function() { function foo() { console.log(this.age); // 这里的this属于函数 foo; 打印 38 } foo(); // foo函是Window对象调用的 }};obj.getAge();面试题2:// 只看函数是怎么被调用的,而不管函数是怎么来的var age = 38;var obj = { age: 18, getAge: function() { alert(this.age); }};var f = obj.getAge;f(); // 函数是Window对象调用的,所以this指向Window对象。打印:38面试题3:var length = 10;function fn(){ console.log(this.length);}var obj = { length: 5, method: function (fn) { fn(); // window对象调用 打印 10 arguments0; // 方法调用模式,是arguments对象调用的 // this指向arguments,所以arguments.length = 2; (arguments.length:实参的个数)所以打印 2 }};obj.method(fn, 123);面试题4:怎么使用call或者apply方法实现构造函数的复用呢?function Person(name, age, gender) { this.name = name; this.age = age; this.gender = gender;}function Teacher(name, age, gender, workYear, subject) { this.name = name; this.age = age; this.gender = gender; this.workYear = workYear; this.subject = subject;}function Student(name, age, gender, stuNo, score) { this.name = name; this.age = age; this.gender = gender; this.stuNo = stuNo; this.score = score;}var tec = new Teacher(‘张老师’, 32, ‘male’, ‘7年’, ‘语文’);var stu = new Student(‘xiaowang’, 18, ‘male’, 10001, 99);console.log(tec); // Teacher {name: “张老师”, age: 32, gender: “male”, workYear: “7年”, subject: “语文”}console.log(stu); // Student {name: “xiaowang”, age: 18, gender: “male”, stuNo: 10001, score: 99}上面的代码中一个Teacher构造函数,一个Student构造函数,他们都有一些公共的属性,跟Person构造函数里面的属性重复,我们能否使用call或者apply方法,简化上面的代码呢?function Person(name, age, gender) { this.name = name; this.age = age; this.gender = gender;}function Teacher(name, age, gender, workYear, subject) { // 借用 Person 函数来给当前对象添加属性 Person.call(this, name, age, gender); // 这里的this指向的就是当前的Teacher构造函数 this.workYear = workYear; this.subject = subject;}function Student(name, age, gender, stuNo, score) { Person.call(this, name, age, gender); // 这里的this指向的就是当前的Student构造函数 this.stuNo = stuNo; this.score = score;}var tec = new Teacher(‘张老师’, 32, ‘male’, ‘7年’, ‘语文’);var stu = new Student(‘xiaowang’, 18, ‘male’, 10001, 99);console.log(tec); // Teacher {name: “张老师”, age: 32, gender: “male”, workYear: “7年”, subject: “语文”}console.log(stu); // Student {name: “xiaowang”, age: 18, gender: “male”, stuNo: 10001, score: 99}11.递归11.1 什么是递归什么是递归?递归就是函数直接自己调用自己或者间接的调用自己。举个例子:函数直接调用自己function fn(){ fn();}fn();函数间接调用自己function fn1(){ fn2();}function fn2(){ fn1();}递归示例代码:function fn (){ console.log(‘从前有座山,’); console.log(‘山里有座庙,’); console.log(‘庙里有个老和尚,’); console.log(‘老和尚给小和尚讲,’); fn();}fn(); // 产生递归,无限打印上面的内容这样做会进入到无限的死循环当中。11.2 化归思想化归思想是将一个问题由难化易,由繁化简,由复杂化简单的过程称为化归,它是转化和归结的简称。合理使用递归的注意点:函数调用了自身必须有结束递归的条件,这样程序就不会一直运行下去了示例代码: 求前n项的和求前n项的和其实就是:1 + 2 + 3 +…+ n;寻找递推关系,就是n与n-1, 或n-2之间的关系:sum(n) == n + sum(n - 1);加上结束的递归条件,不然会一直运行下去。function sum(n){ if(n == 1) return 1; // 递归结束条件 return n + sum(n - 1);}sum(100); // 打印 5050递推关系:11.3 递归练习1、求n的阶乘:思路:f(n) = n * f(n - 1);f(n - 1) = (n - 1) * f(n - 2);示例代码:function product(n){ if(n == 1) { return 1; } return n * product(n-1);}console.log(product(5)); // 打印 1202、求m的n次幂:思路:f(m,n) = m * f(m,n-1);示例代码:function pow(m,n){ if(n==1){ return m; } return m * pow(m,n-1);}console.log(pow(2, 10)); // 打印 10243、斐波那契数列思路:什么是斐波那契数列?1 , 1 , 2 , 3 , 5 , 8 , 13 , 21 , 34 , 55,… 数字从第三项开始,每一项都等于前两项的和。可得出公式:fn = f(n-1) + f(n-2),结束递归的条件:当n <= 2时,fn = 1。示例代码:function fib(n){ if(n<=2) return 1; // 结束递归的条件 return fib(n-1) + fib(n-2);}console.log(fib(5)); // 5console.log(fib(10)); // 55console.log(fib(25)); // 75025 // 数值太大会影响性能问题存在问题:数值太大时会影响性能,怎么影响的呢?function fib(n){ if(n<=2) return 1; return fib(n-1) + fib(n-2); // 当我们在计算一个值的时候,都是通过计算他的fib(n-1) 跟 fib(n-2)项之后再去进行相加,得到最终的值 // 这时候就需要调用两次这个函数,在计算fib(n-1)的时候,其实也是调用了两次这个函数,得出fib(n-1)的值}// 记录执行的次数var count=0;function fib(n){ count++; if(n<=2) return 1; return fib(n-1)+fib(n-2);}console.log(fib(5)); // 5console.log(count); // 9 求第五项的时候就计算了9次//console.log(fib(20)); // 6765//console.log(count); // 13529 求第20项的时候就计算了13529次这个问题在下面讲闭包的时候解决。4.获取页面所有的元素,并加上边框页面结构:<!DOCTYPE html><html lang=“en”><head> <meta charset=“UTF-8”> <meta name=“viewport” content=“width=device-width, initial-scale=1.0”> <meta http-equiv=“X-UA-Compatible” content=“ie=edge”> <title>Document</title></head><body> <div> <p> <span>我是span标签</span> <span>我是span标签</span> <span>我是span标签</span> </p> <p> <span>我是span标签</span> <span>我是span标签</span> <span>我是span标签</span> </p> </div> <div> <p> <span>我是span标签</span> <span>我是span标签</span> <span>我是span标签</span> </p> <p> <span>我是span标签</span> <span>我是span标签</span> <span>我是span标签</span> </p> </div></body></html>结构图:js代码:// 封装一个方法,获取到所有的标签,并且给这些标签加上边框function childrenTag(ele){ var eleArr = []; // 用于存放所有的获取到的标签 var elements = ele.children; // 获取传入元素下的直接子元素 (伪数组) for(var i = 0; i < elements.length; i++){ eleArr.push(elements[i]); // 获取子元素下的直接子元素 var temp = childrenTag(elements[i]); // 一层层的递推下去 eleArr = eleArr.concat(temp); // 将获取的子元素的拼接到一起 } return eleArr;}console.log(childrenTag(document.body)); // 打印的就是页面body下所有的标签// 获取所有标签var tags=childrenTag(document.body);// 给所有标签添加边框for(var i=0;i<tags.length;i++){ tags[i].style.border=‘1px solid cyan’;}效果图:12. JS 内存管理本章引用自:《MDN-内存管理》12.1 内存生命周期不管是什么程序语言,内存生命周期基本是一致的:分配你所需要的内存;使用分配到的内存(读、写);不需要时将其释放、归还。JavaScript 的内存分配:为了不让程序员费心分配内存,JavaScript在定义变量时就完成了内存分配。var n = 123; // 给数值变量分配内存var s = “Levi”; // 给字符串分配内存var o = { a: 1, b: null}; // 给对象及其包含的值分配内存// 给数组及其包含的值分配内存(就像对象一样)var a = [1, null, “abra”]; function f(a){ return a + 2;} // 给函数(可调用的对象)分配内存// 函数表达式也能分配一个对象someElement.addEventListener(‘click’, function(){ someElement.style.backgroundColor = ‘blue’;}, false);使用值:使用值的过程实际上是对分配内存进行读取与写入的操作。读取与写入可能是写入一个变量或者一个对象的属性值,甚至传递函数的参数。当内存不再需要使用时释放:大多数内存管理的问题都在这个阶段。在这里最艰难的任务是找到“所分配的内存确实已经不再需要了”。它往往要求开发人员来确定在程序中哪一块内存不再需要并且释放它。高级语言解释器嵌入了“垃圾回收器”,它的主要工作是跟踪内存的分配和使用,以便当分配的内存不再使用时,自动释放它。这只能是一个近似的过程,因为要知道是否仍然需要某块内存是无法判定的(无法通过某种算法解决)。12.2 垃圾回收如上所述,自动寻找是否一些内存“不再需要”的问题是无法判定的。因此,垃圾回收实现只能有限制的解决一般问题。本节将解释必要的概念,了解主要的垃圾回收算法和它们的局限性。1、引用:垃圾回收算法主要依赖于引用的概念。在内存管理的环境中,一个对象如果有访问另一个对象的权限(隐式或者显式),叫做一个对象引用另一个对象。例如,一个Javascript对象具有对它原型的引用(隐式引用)和对它属性的引用(显式引用)。在这里,“对象”的概念不仅特指JavaScript对象,还包括函数作用域(或者全局词法作用域)。2、引用计数垃圾收集:这是最天真的垃圾收集算法。此算法把“对象是否不再需要”简化定义为“对象有没有其他对象引用到它”。如果没有引用指向该对象(零引用),对象将被垃圾回收机制回收。示例代码var o = { a: { b:2 }}; // 两个对象被创建,一个作为另一个的属性被引用,另一个被分配给变量o// 很显然,没有一个可以被垃圾收集var o2 = o; // o2变量是第二个对“这个对象”的引用o = 1; // 现在,“这个对象”的原始引用o被o2替换了var oa = o2.a; // 引用“这个对象”的a属性// 现在,“这个对象”有两个引用了,一个是o2,一个是oao2 = “yo”; // 最初的对象现在已经是零引用了 // 他可以被垃圾回收了 // 然而它的属性a的对象还在被oa引用,所以还不能回收oa = null; // a属性的那个对象现在也是零引用了 // 它可以被垃圾回收了限制:循环引用该算法有个限制:无法处理循环引用。在下面的例子中,两个对象被创建,并互相引用,形成了一个循环。它们被调用之后会离开函数作用域,所以它们已经没有用了,可以被回收了。然而,引用计数算法考虑到它们互相都有至少一次引用,所以它们不会被回收。function f(){ var o = {}; var o2 = {}; o.a = o2; // o 引用 o2 o2.a = o; // o2 引用 o return “azerty”;}f();实际例子:IE 6, 7 使用引用计数方式对DOM对象进行垃圾回收。该方式常常造成对象被循环引用时内存发生泄漏:var div;window.onload = function(){ div = document.getElementById(“myDivElement”); div.circularReference = div; div.lotsOfData = new Array(10000).join(”*”);};在上面的例子里,myDivElement这个DOM元素里的circularReference属性引用了myDivElement,造成了循环引用。如果该属性没有显示移除或者设为null,引用计数式垃圾收集器将总是且至少有一个引用,并将一直保持在内存里的DOM元素,即使其从DOM树中删去了。如果这个DOM元素拥有大量的数据(如上的lotsOfData属性),而这个数据占用的内存将永远不会被释放。3、标记-清除算法这个算法把“对象是否不再需要”简化定义为“对象是否可以获得”。这个算法假定设置一个叫做根(root)的对象(在Javascript里,根是全局对象)。垃圾回收器将定期从根开始,找所有从根开始引用的对象,然后找这些对象引用的对象……从根开始,垃圾回收器将找到所有可以获得的对象和收集所有不能获得的对象。这个算法比前一个要好,因为“有零引用的对象”总是不可获得的,但是相反却不一定,参考“循环引用”。从2012年起,所有现代浏览器都使用了标记-清除垃圾回收算法。所有对JavaScript垃圾回收算法的改进都是基于标记-清除算法的改进,并没有改进标记-清除算法本身和它对“对象是否不再需要”的简化定义。循环引用不再是问题了在上面的示例中,函数调用返回之后,两个对象从全局对象出发无法获取。因此,他们将会被垃圾回收器回收。第二个示例同样,一旦div和其事件处理无法从根获取到,他们将会被垃圾回收器回收。限制: 那些无法从根对象查询到的对象都将被清除尽管这是一个限制,但实践中我们很少会碰到类似的情况,所以开发者不太会去关心垃圾回收机制。一般情况下, 如果需要手动释放变量占用的内存, 就将这个变量赋值为:null13. 闭包了解闭包之前,先了解下另外两个知识点:1、函数基础知识1、函数内部的代码在调用的时候执行2、函数返回值类型可以是任意类型3、怎么理解函数的返回值将函数内部声明的变量暴露到函数外部函数内用来返回数据,相当于没有函数的时候直接使用该数据不同之处在于:函数形成作用域,变量为局部变量function foo() { var o = {age: 12}; return o;}var o1 = foo();// 相当于: var o1 = {age: 18};2、作用域的结论1、JavaScript的作用域是词法作用域2、函数才会形成作用域(函数作用域)3、词法作用域:变量(变量和函数)的作用范围在代码写出来的就已经决定, 与运行时无关4、函数内部可以访问函数外部的变量(函数外部不能访问函数内部的变量)5、变量搜索原则:从当前链开始查找直到0级链6、当定义了一个函数,当前的作用域链就保存起来,并且成为函数的内部状态的一部分。13.1 闭包的概念闭包从字面意思理解就是闭合,包起来。简单的来说闭包就是,一个具有封闭的对外不公开的包裹结构或空间。在JavaScript中函数可以构成闭包。一般函数是一个代码结构的封闭结构,即包裹的特性。同时根据作用域规则, 只允许函数访问外部的数据,外部无法访问函数内部的数据,即封闭的对外不公开的特性。因此说函数可以构成闭包。闭包的其他解释在计算机科学中,闭包(英语:Closure),又称词法闭包(Lexical Closure)或函数闭包(function closures),是引用了自由变量的函数。这个被引用的自由变量将和这个函数一同存在,即使已经离开了创造它的环境也不例外。所以,有另一种说法认为闭包是由函数和与其相关的引用环境组合而成的实体。闭包在运行时可以有多个实例,不同的引用环境和相同的函数组合可以产生不同的实例。实例:function fn() { var num = 123; return function foo() { console.log(num); };}// bar1就是闭包的一个实例var bar1 = fn();// bar2就是闭包的另外一个实例var bar2 = fn();bar1(); // 123bar2(); // 123闭包的构成闭包包括两部分:1、函数体(函数自身的代码);2、环境(函数的作用域)。闭包的说明1、JS中函数形成了闭包2、闭包是函数作用域的应用3、对于闭包来说,只关注创建函数的作用域,不关注调用函数的位置闭包的作用对函数内部的变量起到保护作用除了返回的函数以外,没有任何手段能够获取或者修改这个变量的值13.2 闭包模型function foo() { var num = 0; // 函数会产生一个作用域,所以外部的程序想要访问函数内部的变量,一般情况下是不行的 // 通过闭包的方式可以使外部访问到函数内部的变量 // 具体做法就是在函数内部返回一个函数,并且这个函数使用了这个变量 // 当用户调用最外层的函数的时候,使用的这个变量就会随着返回的函数返回给用户 return function() { return ++num; };}// 函数foo的返回值就是一个函数,所以,就可以调用getNum这个函数了!var getNum = foo();console.log(getNum()); // 113.3 闭包的使用目标:想办法(在外部)访问到函数内部的数据利用函数返回值function foo() { var num = Math.random(); return num;}var num1 = foo();var num2 = foo();console.log(num1 === num2); // 随机数 相同的情况很小很小普通的函数返回值说明两次调用函数,返回的数据并不是同一个数据。原因:函数在每次调用的时候,内部的数据会被新创建一次游戏充值案例示例图片:示例代码:<button id=“pay”>充值</button><button id=“play”>玩游戏</button><script> // 需求: // 1-需要对充值的金额起到保护作用,这个存放数值的变量不能暴露在全局,否则谁都会去修改这个金额 // var money = 0; // 2-点击充值按钮的时候,每次充值10元 // 3-点击玩游戏按钮的时候,每玩一次金额减少一元 function fn (){ var money = 0; // money用来存储充值的钱,放在函数内部,不会暴露在全局 // 一般的闭包返回值是一个函数,但是这里有两个功能,一个是玩游戏,一个是充值; // 两个功能分开,但是金额之间还是关联的,所以这里返回一个对象,里面存放两个方法 return { // 充值的函数 recharge:function(value){ money += value; console.log(‘尊敬的黄金会员,您本次充值:’ + value, ‘,您的总余额为:’ + money); }, // 玩游戏的函数 play:function(){ if(money <= 0){ console.log(‘余额不足无法继续游戏,请充值!’); return; } money–; console.log(‘您还剩余 ’ + money + ’ 条命!’); } }; } var obj = fn(); // 点击“充值”按钮 var pay = document.getElementById(‘pay’); pay.addEventListener(‘click’, function () { obj.recharge(10); }); // 点击“玩游戏”按钮 var play = document.getElementById(‘play’) play.addEventListener(‘click’, function () { obj.play(); })</script>优化,多个角色进行充值玩游戏<div> <button id=“pay”>小明:充值</button> <button id=“play”>小明:玩游戏</button></div><div> <button id=“pay1”>小华:充值</button> <button id=“play1”>小华:玩游戏</button></div><script> // 1 需要对充值的钱起到保护作用 // var money = 0; // 2 充值: 每次充值20 // 3 玩游戏: 每玩一次,金额少1 // 整个fn()形成一个函数作用域,对里面的变量起到保护作用 function fn() { // money 用来存储充值的钱 var money = 0; // 充值的函数: function recharge(value) { // money += 20; money += value; console.log(‘尊敬的黄金会员,您本次充值:’ + value, ‘,您的总余额为:’ + money); } // 玩游戏的函数 function play() { money–; if (money < 0) { console.log(‘余额不足,请充值!’); } else { console.log(‘您还剩余 ’ + money + ’ 条命!’); } } return { recharge: recharge, play: play }; } // 小明充值玩游戏的函数 var obj; obj = fn(); // 小明玩游戏: var pay = document.getElementById(‘pay’); pay.addEventListener(‘click’, function () { obj.recharge(20); }); var play = document.getElementById(‘play’) play.addEventListener(‘click’, function () { obj.play(); }); // 小华(新的闭包实例): var obj1 = fn(); // 小华玩游戏: var pay1 = document.getElementById(‘pay1’); pay1.addEventListener(‘click’, function () { obj1.recharge(20); }); var play1 = document.getElementById(‘play1’) play1.addEventListener(‘click’, function () { obj1.play(); });</script>优化的案例我们可以看到,只要重新定义一个变量,接收函数 fn(),就能重新开辟一个新的空间,且多个用户之间不受任何影响。13.4 闭包里的缓存从内存看闭包函数调用也是需要内存的!因为函数中声明了一些变量,这些变量在函数调用过程中是可以使用的,所以, 这个变量是存储到了函数调用时候分配的内存中了!因为没有任何变量来引用这块内存,所以,函数调用结束。 函数调用占用的内存就会被回收掉。虽然,此时的函数有返回值(返回了一个普通的变量),并且这个函数调用结束以后这个函数占用的内存还是被回收了!但是, 存储函数的内存还在。闭包的内存占用:作用域的引用是对函数整个作用域来说的,而不是针对作用域中的某个变量!!!即便没有任何的变量,也是有作用域( 作用域的引用 )。function fn() { var num = 123; return function() { console.log(num); };}// 此时, 函数fn调用时候占用的内存, 是不会被释放掉的!!!var foo = fn();// 调用 foo() 此时, 因为返回函数的作用域对外层函数fn的作用域有引用// 所以, 即使是 fn() 调用结束了, 因为 返回函数作用域引用的关系, 所以// 函数fn()调用时候, 产生的内存是不会被释放掉的!foo();// 手动释放闭包占用的内存!foo = null;缓存介绍缓存:暂存数据方便后续计算中使用。缓存中存储的数据简单来说就是:键值对工作中,缓存是经常被使用的手段。目的:提高程序运行的效率我们只要是使用缓存,就完全信赖缓存中的数据。所以, 我们可以通过闭包来保护缓存。对于缓存来说,我们既要存储值,又要取值!存储的目的是为了将来取出来,在js中可以使用对象或者数组来充当缓存。如果是需要保持顺序的,那么就用数组,否则就用对象!// 创建一个缓存:var cache = {};// 往缓存中存数据:cache.name = ‘xiaoming’;cache[’name1’] = ‘xiaohua’;// 取值console.log(cache.name);console.log(cache[’name1’]);计算机中的缓存就是数据交换的缓冲区(称作Cache),当某一硬件要读取数据时,会首先从缓存中查找需要的数据,如果找到了则直接执行,找不到的话则从内存中找。由于缓存的运行速度比内存快得多,故缓存的作用就是帮助硬件更快地运行。缓存使用步骤首先查看缓存中有没有该数据,如果有,直接从缓存中取出来;如果没有就递归计算,并将结果放到缓存中递归计算斐波那契数列存在的问题前面在学习递归的时候,我们举了一个斐波那契数列的例子,但是当时说存在性能问题,我们重新看下这个问题。// 使用递归计算 菲波那契数列// 数列:1 1 2 3 5 8 13 21 34 55 89 。。。// 索引:0 1 2 3 4 5 6 7 8 9 10 。。。var count = 0;var fib = function (num) { count++; if (num === 0 || num == 1) { return 1; } return fib(num - 1) + fib(num - 2);};// 计算索引号为10的值, 一共计算了: 177 次// 计算索引号为11的值, 一共计算了: 287 次// 计算索引号为12的值, 一共计算了: 465 次// ….// 计算索引号为20的值, 一共计算了: 21891 次// 计算索引号为21的值, 一共计算了: 35421 次// …// 计算索引号为30的值, 一共计算了: 2692537 次// 计算索引号为31的值, 一共计算了: 4356617 次fib(31);console.log(count); // 4356617注意上面代码,count是用来记录程序运行时执行的次数,不明白的小伙伴可以返回递归那一章节,我专门画了一张图,可以理解下这个次数是怎么计算的。我们看下上面的代码的注释,求第20项跟21项的时候,虽然只相差一项,但是却多运算了一万多次,试想一下这里面存在的效率问题是多么的可怕。闭包和缓存解决计算斐波那契数列存在的问题其实主要的问题就是,数据重复运算。比如计算第五项的时候,他计算的是第三项跟第四项的和,这时的第三项跟第四项都是从一开始重新计算的,假如吧计算过得值保存下来,就不需要再重复的运算。运用缓存:将计算的值存储下来,减少运算次数,提高效率;使用闭包:从来保护缓存。// 记录计算的次数var count = 0;function fn() { // 缓存对象 var cache = {}; // 这个返回函数才是 递归函数! return function( num ) { count++; // 1 首先查看缓存中有没有 num 对应的数据 if(cache[num]) { // 说明缓存中有我们需要的数据 return cache[num]; } // 2 如果缓存中没有, 就先计算, 并且将计算的结果存储到缓存中 if(num === 0 || num === 1) { // 存储到缓存中 cache[num] = 1; return 1; } var temp = arguments.callee(num - 1) + arguments.callee(num - 2); cache[num] = temp; return temp; };}var fib = fn();var ret = fib(20);console.log(ret); // 10946console.log(‘计算了:’ , count, ‘次’); // 计算了: 39 次我们可以跟上面没有使用缓存,求斐波那契数列的比较一下,此时求第20项的时候,仅仅运算了39次,但是在之前却运行了21891次。上面的方法存在着一些的问题,每次在执行的时候,函数fn都要先被调用一次(var fib = fn();),下面进行优化:将fn转换成自执行函数(沙箱模式,下一章会讲),自执行函数的返回函数就是递归函数;判断缓存是否存在的条件进行优化,之前是通过判断缓存的值是否存在,来进行存、取值的,但是假如一个缓存的值是false的时候呢?岂不是if(false){}了,明明有值的时候,却不能取值了,所以玩我们只需要判断缓存里是否存在某个键就行。var fib = (function () { // 缓存对象 var cache = {}; // 这个返回函数才是 递归函数! return function (num) { // 1 首先查看缓存中有没有 num 对应的数据 /** if(cache[num]) { return cache[num]; } / // 只要缓存对象中存在 num 这个key, 那么结果就应该是 true if (num in cache) { // 说明缓存中有我们需要的数据 return cache[num]; } // 2 如果缓存中没有, 就先计算, 并且将计算的结果存储到缓存中 if (num === 0 || num === 1) { // 存储到缓存中 // cache[num] = 1 是一个赋值表达式, 赋值表达式的结果为: 等号右边的值! return (cache[num] = 1); } // arguments.callee 表示当前函数的引用 return (cache[num] = arguments.callee(num - 1) + arguments.callee(num - 2)); };})();var ret = fib(10)console.log(ret);什么是 arguments.callee?返回正被执行的function对象,也就是所指定的function对象的正文。callee属性是arguments 对象的一个成员,它表示对函数对象本身的引用,这有利于匿名函数的递归或者保证函数的封装性。function fn(a, b) { console.log(arguments);}fn(1, 2);我们可以看到,打印的arguments属性里面有哪些参数:前面几项是函数调用后传进来的实参;callee:f,它其实就是函数fn的引用,你可以理解为:arguments.callee()相当于fn();length就是实参的长度。再去看上面斐波那契的案例,它的递归函数是一个匿名函数,所以在这个函数里面自己调用自己的时候,就是使用的arguments.callee去引用的。14. 沙箱模式沙箱模式又称:沙盒模式、隔离模式。沙箱(sandbox)介绍:用于为一些来源不可信、具备破坏力或无法判定程序意图的程序提供试验环境。然而,沙盒中的所有改动对操作系统不会造成任何损失。14.1 沙箱模式的作用作用:对变量进行隔离问题:在js中如何实现隔离?ES6之前, JavaScritp中只有函数能限定作用域,所以,只有使用函数才能实现隔离。本质上还是对函数作用域的应用。14.2 沙箱模式模型使用自调用函数实现沙箱模式函数形成独立的作用域;函数只有被调用,内部代码才会执行;将全局污染降到最低。(function() { // … // 代码 // …})(); 14.3 沙箱模式应用最佳实践:在函数内定义变量的时候,将 变量定义 提到最前面。// 1 减少了window变量作用域的查找// 2 有利于代码压缩(function( window ) { var fn = function( selector ) { this.selector = selector; }; fn.prototype = { constructor: fn, addClass: function() {}, removeClass: function() {} }; // 给window添加了一个 $属性,值为: fn // 暴露数据的方式: window.$ = fn;})( window );14.4 沙箱模式的说明将代码放到一个立即执行的函数表达式(IIFE)中,这样就能实现代码的隔离;使用IIFE:减少一个函数名称的污染,将全局变量污染降到最低;代码在函数内部执行,函数内部声明的变量不会影响到函数外部;如果外部需要,则可以返回数据或把要返回的数据交给window。IIFE: Immediately Invoke Function Expression立即执行的函数表达式15. 工厂模式工厂模式是一种设计模式,作用是:隐藏创建对象的细节,省略了使用new创建对象。构造函数:构造函数创建之后,我们实例化一个对象的时候都是直接通过new创建出来的。function Person(name, age) { this.name = name; this.age = age;}var p1 = new Person(‘Levi’, 18); 工厂函数:工厂函数的核心就是隐藏这个new创建对象的细节。function Person(name, age) { this.name = name; this.age = age;}function createPerson(name, age) { return new Person(name, age);}var p2 = createPerson(‘Ryan’, 19);两段代码比较下来,我们可以看到,实例出来的p2对象没有直接使用new创建,而是通过一个函数的返回值创建出来的,这就是工厂模式。使用场合:jQuery中,我们用的“$”或者jQuery函数,就是一个工厂函数。/ Jquery 中的部分源码 */// jQuery 实际上是一个 工厂函数,省略了 new 创建对象的操作jQuery = function( selector, context ) { // jQuery.fn.init 才是jQuery中真正的构造函数 return new jQuery.fn.init( selector, context );}(本篇完) ...

December 27, 2018 · 19 min · jiezi

JS 总结之原型继承方式收录

以一个父类为前提条件,列举 js 继承的继承方式:function Person (age) { this.age = age || 18}Person.prototype.sleep = function () { console.log(‘sleeping’)}???? 方式 1:原型链继承(不推荐)function Programmer() {}Programmer.prototype = new Person ()Programmer.prototype.code = function () { console.log(‘coding’)}let jon = new Programmer()jon.code() // codingjon.sleep() // sleepingjon instanceof Person // truejon instanceof Programmer // trueObject.getPrototypeOf(jon) // Person {age: 18, code: ƒ}jon.proto // Person {age: 18, code: ƒ}缺点:无法向父类构造函数传参父类的所有属性被共享,只要一个实例修改了属性,其他所有的子类实例都会被影响???? 方式 2:借用构造函数(经典继承)(不推荐)复制父类构造函数内的属性function Programmer(name) { Person.call(this) this.name = name}let jon = new Programmer(‘jon’)jon.name // jonjon.age // 18jon.sleep() // Uncaught TypeError: jon.sleep is not a functionjon instanceof Person // falsejon instanceof Programmer // true优点:可以为父类传参避免了共享属性缺点:只是子类的实例,不是父类的实例方法都在构造函数中定义,每次创建实例都会创建一遍方法???? 方式 3:组合继承(推荐)组合 原型链继承 和 借用构造函数继承。function Programmer(age, name) { Person.call(this, age) this.name = name}Programmer.prototype = new Person()Programmer.prototype.constructor = Programmer // 修复构造函数指向let jon = new Programmer(18, ‘jon’)jon.age // 18jon.name // jonlet flash = new Programmer(22, ‘flash’)flash.age // 22flash.name // flashjon.age // 18jon instanceof Person // truejon instanceof Programmer // trueflash instanceof Person // trueflash instanceof Programmer // true优点:融合原型链继承和构造函数的优点,是 JavaScript 中最常用的继承模式缺点:调用了两次父类构造函数???? 方式 4:原型式继承(不推荐)function create(o) { function F() {} F.prototype = o return new F()}let obj = { gift: [‘a’, ‘b’]}let jon = create(obj)let xiaoming = create(obj)jon.gift.push(‘c’)xiaoming.gift // [‘a’, ‘b’, ‘c’]缺点:共享了属性和方法???? 方式 5:寄生式继承(不推荐)创建一个仅用于封装继承过程的函数,该函数在内部以某种形式来做增强对象,最后返回对象function createObj (o) { var clone = Object.create(o) clone.sayName = function () { console.log(‘hi’) } return clone}缺点:跟借用构造函数模式一样,每次创建对象都会创建一遍方法???? 方式 6:寄生组合继承(最佳)子类构造函数复制父类的自身属性和方法,子类原型只接受父类的原型属性和方法:function create(prototype) { function Super() {} Super.prototype = prototype return new Super()}function Programmer(age, name) { Person.call(this, age) this.name = name}Programmer.prototype = create(Person.prototype)Programmer.prototype.constructor = Programmer // 修复构造函数指向let jon = new Programmer(18, ‘jon’)jon.name // jon进阶封装:function create(prototype) { function Super() {} Super.prototype = prototype return new Super()}function prototype(child, parent) { let prototype = create(parent.prototype) prototype.constructor = child // 修复构造函数指向 child.prototype = prototype}function Person (age) { this.age = age || 18}Person.prototype.sleep = function () { console.log(‘sleeping’)}function Programmer(age, name) { Person.call(this, age) this.name = name}prototype(Programmer, Person)let jon = new Programmer(18, ‘jon’)jon.name // jon引用《JavaScript 高级程序设计》中对寄生组合式继承的夸赞就是:这种方式的高效率体现它只调用了一次 Parent 构造函数,并且因此避免了在 Parent.prototype 上面创建不必要的、多余的属性。与此同时,原型链还能保持不变;因此,还能够正常使用 instanceof 和 isPrototypeOf。开发人员普遍认为寄生组合式继承是引用类型最理想的继承范式。???? 方式 7:ES6 extends(最佳)// 父类class Person { constructor(age) { this.age = age } sleep () { console.log(‘sleeping’) }}// 子类class Programmer extends Person { constructor(age, name) { super(age) this.name = name } code () { console.log(‘coding’) }}let jon = new Programmer(18, ‘jon’)jon.name // jonjon.age // 18let flash = new Programmer(22, ‘flash’)flash.age // 22flash.name // flashjon instanceof Person // truejon instanceof Programmer // trueflash instanceof Person // trueflash instanceof Programmer // true优点:不用手动设置原型。缺点:新语法,只要部分浏览器支持,需要转为 ES5 代码。???? 参考《JavaScript 高级程序设计(第 3 版)》6.3 继承《JavaScript 深入之继承的多种方式和优缺点》 by 冴羽《ECMAScript 6 入门》Class 的继承 by 阮一峰 ...

December 23, 2018 · 2 min · jiezi

javascript面向对象与原型

昨天我们讲了在面向对象中创建对象的几种方式工厂模式构造函数模式工厂模式创建的对象,像工厂一样来创建对象,创建的每一个对象都是通过new Object()来创建的,原型直指Object()构造函数似乎不错,但有的时候我们需要对属性和方法进行修改,属性vue的同学应该都遇到过这种情况,我们需要声明一些全局变量,我们一般这么做//封装全局的ajax请求 Vue.prototype.$http = function (url, options) { //部分代码省略 return fetch(url, options) } // 注入 全局的wxSDK Vue.prototype.$wx = Wx这时候就用到了原型我之前就用了大量篇幅讲过javascript的原型,这次遇到了面向对象,换个角度再次讨论原型模式创建对象我们创建的每个函数都有一个prototype(原型)属性,这个属性是一个指针,指向一个对象,而这个对象的用途是包含可以由特定类型的所有实例共享的属性和方法。使用原型对象的好处是可以让所有对象实例共享它所包含的属性和方法。也就是说,不用再构造函数中定义对象的实例信息,而是将这些属性和方法添加到原型对象中一个????:function Hero() {}Hero.prototype.name = “欧阳锋"Hero.prototype.nickname = “西毒"Hero.prototype.doSth = function () { console.log(‘学习九阴真经’);}const hero1 = new Hero()console.log(hero1.name); //=>欧阳锋console.log(hero1.nickname); //=>西毒hero1.doSth() //=>学习九阴真经const hero2 = new Hero()console.log(hero2.name); //=>欧阳锋console.log(hero2.nickname); //=>西毒hero2.doSth() //=>学习九阴真经我们看下hero1console.log(hero1);我们可以通过对象实例访问保存在原型中的值,但是我们不能通过对象实例重写原型中的值,强制的重写也可以function Hero() {}Hero.prototype.name = “欧阳锋"Hero.prototype.nickname = “西毒"Hero.prototype.doSth = function () { console.log(‘学习九阴真经’);}const hero1 = new Hero()hero1.proto.name=‘黄药师’console.log(hero1.name);//=>黄药师const hero2 = new Hero()console.log(hero2.name);//=>黄药师这么做就是修改了原型链,通过原型创建的其他实例也会同步更改,这样不是我们所希望的结果怎么办?实例属性屏蔽同名的原型属性在实例中添加属性,使这个属性和原型中的属性同名,这个属性会屏蔽掉(也可以理解为覆盖掉)原型中的那个属性继续????的????function Hero() {}Hero.prototype.name = “欧阳锋"Hero.prototype.nickname = “西毒"Hero.prototype.doSth = function () { console.log(‘学习九阴真经’);}const hero1 = new Hero()const hero2 = new Hero()hero1.name=“黄药师"console.log(hero1.name);//=>黄药师console.log(hero2.name);//=>欧阳锋我们看下hero1console.log(hero1);黄药师在实例中,欧阳锋在原型中,我们访问hero1.name,会先在实例上去搜索name属性,实例上存在name属性,就返回实例上的name属性,停止搜索。我们再去访问hero2.name,同样的会在实例上进行搜索name属性,但是没搜到,继续顺着原型链进行搜索,搜到了,返回原型中的name属性。我们在原型中添加一个属性,这个属性会屏蔽掉原型中的同名属性,也就是说会阻止我们访问原型中的同名属性,但是不会修改,如果我们把这个属性设置为null,会怎么样呢?//部分代码省略const hero1 = new Hero()const hero2 = new Hero()hero1.name=nullconsole.log(hero1.name);//=>null(来自实例)console.log(hero2.name);//=>欧阳锋(来自原型)所以说,就算设置为null,结论是和上面是相同的,只要在实例上存在这个要访问的属性,就会停止搜索实例中的属性我不想要了怎么办,设置null也无效,使用delete删除//部分代码省略const hero1 = new Hero()const hero2 = new Hero()hero1.name=nulldelete(hero1.name)//删除实例中的属性console.log(hero1.name);//=>欧阳锋(来自原型)console.log(hero2.name);//=>欧阳锋(来自原型)判断属性属于实例还是原型有个内置方法hasOwnProperty()用来判断属性是否来自于实例,属于实例则返回true,否则false//部分代码省略const hero1 = new Hero()const hero2 = new Hero()hero1.name=‘黄药师’// console.log(hero1.name);// console.log(hero2.name);console.log(hero1.hasOwnProperty(’name’));//=>trueconsole.log(hero2.hasOwnProperty(’name’));//=>false还有一个in操作符,如果对象能访问到指定属性就返回true//部分代码省略const hero1 = new Hero()const hero2 = new Hero()hero1.name=‘黄药师’console.log(’name’ in hero1);//=>trueconsole.log(’name’ in hero2);//=>true我们将in和hasOwnProperty()结合在一起,封装一个方法用来判断属性来自原型还是实例const hero1 = new Hero()const hero2 = new Hero()hero1.name = ‘黄药师’//true=>来自原型//false=>来自实例function attrFromProto(obj, attr) { return !obj.hasOwnProperty(attr) && (attr in obj)}console.log(attrFromProto(hero1, ’name’));//=>false(来自实例)console.log(attrFromProto(hero2, ’name’));//=>true(来自原型)原型模式创建对象的简化版????我们创建对象是这样的function Hero() {}Hero.prototype.name = “欧阳锋"Hero.prototype.nickname = “西毒"Hero.prototype.doSth = function () { console.log(‘学习九阴真经’);}我能重复的写prototype,我们结合对象字面量简化一下function Hero() {}Hero.prototype = { name: “欧阳锋”, nickname: “西毒”, doSth: function () { console.log(‘学习九阴真经’); }}通过原型模式创建对象注意事项在原型中添加公有方法是值得鼓励的,类似于我们在vue中添加全局ajax,添加一下基本类型的变量也可以,但是当添加引用类型的属性时候问题就出现了function Hero() {}Hero.prototype.name = “欧阳锋"Hero.prototype.nickname = “西毒"Hero.prototype.skill = [“灵蛇拳”, “神驼雪山掌”]Hero.prototype.doSth = function () { console.log(‘学习九阴真经’);}const hero1 = new Hero()const hero2 = new Hero()hero1.skill.push(‘灵蛇杖法’)console.log(hero1.skill);//=>[“灵蛇拳”, “神驼雪山掌”, “灵蛇杖法”]console.log(hero2.skill);//=>[“灵蛇拳”, “神驼雪山掌”, “灵蛇杖法”]我只在hero1实例中push了“灵蛇杖法”,结果影响到了hero2,当在实例对象上添加引用类型时要格外小心今天就到这里,明天不见不散收集整理了一些电子书,有需要的,在公众号后台回复“电子书”即可领取原文链接 ...

December 17, 2018 · 1 min · jiezi

一文让你理解什么是 JS 原型

前言最近整理了一部分的JavaScript知识点,由于js高级阶段涉及知识点比较复杂,文章一直没更新,这里单独将原型部分的概念拎出来理解下。1.原型1.1 传统构造函数存在问题通过自定义构造函数的方式,创建小狗对象:function Dog(name, age) { this.name = name; this.age = age; this.say = function() { console.log(‘汪汪汪’); }}var dog1 = new Dog(‘哈士奇’, 1.5);var dog2 = new Dog(‘大黄狗’, 0.5);console.log(dog1);console.log(dog2);console.log(dog1.say == dog2.say); //输出结果为false画个图理解下:每次创建一个对象的时候,都会开辟一个新的空间,我们从上图可以看出,每只创建的小狗有一个say方法,这个方法都是独立的,但是功能完全相同。随着创建小狗的数量增多,造成内存的浪费就更多,这就是我们需要解决的问题。为了避免内存的浪费,我们想要的其实是下图的效果:解决方法:这里最好的办法就是将函数体放在构造函数之外,在构造函数中只需要引用该函数即可。function sayFn() { console.log(‘汪汪汪’);}function Dog(name, age) { this.name = name; this.age = age; this.say = sayFn();}var dog1 = new Dog(‘哈士奇’, 1.5);var dog2 = new Dog(‘大黄狗’, 0.5);console.log(dog1);console.log(dog2);console.log(dog1.say == dog2.say); //输出结果为true这样写依然存在问题:全局变量增多,会增加引入框架命名冲突的风险代码结构混乱,会变得难以维护想要解决上面的问题就需要用到构造函数的原型概念。1.2 原型的概念prototype:原型。每个构造函数在创建出来的时候系统会自动给这个构造函数创建并且关联一个空的对象。这个空的对象,就叫做原型。关键点:每一个由构造函数创建出来的对象,都会默认的和构造函数的原型关联;当使用一个方法进行属性或者方法访问的时候,会先在当前对象内查找该属性和方法,如果当前对象内未找到,就会去跟它关联的原型对象内进行查找;也就是说,在原型中定义的方法跟属性,会被这个构造函数创建出来的对象所共享;访问原型的方式:构造函数名.prototype。示例图:示例代码: 给构造函数的原型添加方法function Dog(name,age){ this.name = name; this.age = age;}// 给构造函数的原型 添加say方法Dog.prototype.say = function(){ console.log(‘汪汪汪’);}var dog1 = new Dog(‘哈士奇’, 1.5);var dog2 = new Dog(‘大黄狗’, 0.5);dog1.say(); // 汪汪汪dog2.say(); // 汪汪汪我们可以看到,本身Dog这个构造函数中是没有say这个方法的,我们通过Dog.prototype.say的方式,在构造函数Dog的原型中创建了一个方法,实例化出来的dog1、dog2会先在自己的对象先找say方法,找不到的时候,会去他们的原型对象中查找。如图所示:在构造函数的原型中可以存放所有对象共享的数据,这样可以避免多次创建对象浪费内存空间的问题。1.3 原型的使用1、使用对象的动态特性使用对象的动态属性,其实就是直接使用prototype为原型添加属性或者方法。function Person () {}Person.prototype.say = function () { console.log( ‘讲了一句话’ );};Person.prototype.age = 18;var p = new Person();p.say(); // 讲了一句话console.log(p.age); // 182、直接替换原型对象每次构造函数创建出来的时候,都会关联一个空对象,我们可以用一个对象替换掉这个空对象。function Person () {}Person.prototype = { say : function () { console.log( ‘讲了一句话’ ); },};var p = new Person();p.say(); // 讲了一句话注意:使用原型的时候,有几个注意点需要注意一下,我们通过几个案例来了解一下。使用对象.属性名去获取对象属性的时候,会先在自身中进行查找,如果没有,就去原型中查找;// 创建一个英雄的构造函数 它有自己的 name 和 age 属性function Hero(){ this.name=“德玛西亚之力”; this.age=18;}// 给这个构造函数的原型对象添加方法和属性Hero.prototype.age= 30;Hero.prototype.say=function(){ console.log(‘人在塔在!!!’);}var h1 = new Hero();h1.say(); // 先去自身中找 say 方法,没有再去原型中查找 打印:‘人在塔在!!!‘console.log(p1.name); // “德玛西亚之力"console.log(p1.age); // 18 先去自身中找 age 属性,有的话就不去原型中找了使用对象.属性名去设置对象属性的时候,只会在自身进行查找,如果有,就修改,如果没有,就添加;// 创建一个英雄的构造函数function Hero(){ this.name=“德玛西亚之力”;}// 给这个构造函数的原型对象添加方法和属性Hero.prototype.age = 18;var h1 = new Hero();console.log(h1); // {name:“德玛西亚之力”}console.log(h1.age); // 18h1.age = 30; // 设置的时候只会在自身中操作,如果有,就修改,如果没有,就添加 不会去原型中操作console.log(h1); // {name:“德玛西亚之力”,age:30}console.log(h1.age); // 30一般情况下,不会将属性放在原型中,只会将方法放在原型中;在替换原型的时候,替换之前创建的对象,和替换之后创建的对象的原型不一致!!!// 创建一个英雄的构造函数 它有自己的 name 属性function Hero(){ this.name=“德玛西亚之力”;}// 给这个构造函数的默认原型对象添加 say 方法Hero.prototype.say = function(){ console.log(‘人在塔在!!!’);}var h1 = new Hero();console.log(h1); // {name:“德玛西亚之力”}h1.say(); // ‘人在塔在!!!’// 开辟一个命名空间 obj,里面有个 kill 方法var obj = { kill : function(){ console.log(‘大宝剑’); }}// 将创建的 obj 对象替换原本的原型对象Hero.prototype = obj;var h2 = new Hero();h1.say(); // ‘人在塔在!!!‘h2.say(); // 报错h1.kill(); // 报错h2.kill(); // ‘大宝剑’画个图理解下:图中可以看出,实例出来的h1对象指向的原型中,只有say()方法,并没有kill()方法,所以h1.kill()会报错。同理,h2.say()也会报错。1.4 __proto__属性在js中以_开头的属性名为js的私有属性,以__开头的属性名为非标准属性。__proto__是一个非标准属性,最早由firefox提出来。1、构造函数的 prototype 属性之前我们访问构造函数原型对象的时候,使用的是prototype属性:function Person(){}//通过构造函数的原型属性prototype可以直接访问原型Person.prototype;在之前我们是无法通过构造函数new出来的对象访问原型的:function Person(){}var p = new Person();//以前不能直接通过p来访问原型对象2、实例对象的 proto 属性__proto__属性最早是火狐浏览器引入的,用以通过实例对象来访问原型,这个属性在早期是非标准的属性,有了__proto__属性,就可以通过构造函数创建出来的对象直接访问原型。function Person(){}var p = new Person();//实例对象的__proto__属性可以方便的访问到原型对象p.proto;//既然使用构造函数的prototype和实例对象的__proto__属性都可以访问原型对象//就有如下结论p.proto === Person.prototype;如图所示:3、__proto__属性的用途可以用来访问原型;在实际开发中除非有特殊的需求,不要轻易的使用实例对象的__proto__属性去修改原型的属性或方法;在调试过程中,可以轻易的查看原型的成员;由于兼容性问题,不推荐使用。3.5 constuctor属性constructor:构造函数,原型的constructor属性指向的是和原型关联的构造函数。示例代码:function Dog(){ this.name=“husky”;}var d=new Dog();// 获取构造函数console.log(Dog.prototype.constructor); // 打印构造函数 Dogconsole.log(d.proto.constructor); // 打印构造函数 Dog如图所示:获取复杂类型的数据类型:通过obj.constructor.name的方式,获取当前对象obj的数据类型。在一个的函数中,有个返回值name,它表示的是当前函数的函数名;function Teacher(name,age){ this.name = name; this.age = age;}var teacher = new Teacher();// 假使我们只知道一个对象teacher,如何获取它的类型呢?console.log(teacher.proto.constructor.name); // Teacherconsole.log(teacher.constructor.name); // Teacher实例化出来的teacher对象,它的数据类型是啥呢?我们可以通过实例对象teacher.proto,访问到它的原型对象,再通过.constructor访问它的构造函数,通过.name获取当前函数的函数名,所以就能得到当前对象的数据类型。又因为.__proto__是一个非标准的属性,而且实例出的对象继承原型对象的方法,所以直接可以写成:obj.constructor.name。1.6 原型继承原型继承:每一个构造函数都有prototype原型属性,通过构造函数创建出来的对象都继承自该原型属性。所以可以通过更改构造函数的原型属性来实现继承。继承的方式有多种,可以一个对象继承另一个对象,也可以通过原型继承的方式进行继承。1、简单混入继承直接遍历一个对象,将所有的属性和方法加到另一对象上。var animal = { name:“Animal”, sex:“male”, age:5, bark:function(){ console.log(“Animal bark”); }};var dog = {};for (var k in animal){ dog[k]= animal[k];}console.log(dog); // 打印的对象与animal一模一样缺点:只能一个对象继承自另一个对象,代码复用太低了。2、混入式原型继承混入式原型继承其实与上面的方法类似,只不过是将遍历的对象添加到构造函数的原型上。var obj={ name:‘zs’, age:19, sex:‘male’ }function Person(){ this.weight=50;}for(var k in obj){ // 将obj里面的所有属性添加到 构造函数 Person 的原型中 Person.prototype[k] = obj[k];}var p1=new Person();var p2=new Person();var p3=new Person();console.log(p1.name); // ‘zs’console.log(p2.age); // 19console.log(p3.sex); // ‘male’面向对象思想封装一个原型继承我们可以利用面向对象的思想,将面向过程进行封装。function Dog(){ this.type = ‘yellow Dog’;}// 给构造函数 Dog 添加一个方法 extendDog.prototype.extend = function(obj){ // 使用混入式原型继承,给 Dog 构造函数的原型继承 obj 的属性和方法 for (var k in obj){ this[k]=obj[k]; }}// 调用 extend 方法Dog.prototype.extend({ name:“二哈”, age:“1.5”, sex:“公”, bark:function(){ console.log(‘汪汪汪’); }});3、替换式原型继承替换式原型继承,在上面已经举过例子了,其实就是将一个构造函数的原型对象替换成另一个对象。function Person(){ this.weight=50;}var obj={ name:‘zs’, age:19, sex:‘male’}// 将一个构造函数的原型对象替换成另一个对象Person.prototype = obj;var p1=new Person();var p2=new Person();var p3=new Person();console.log(p1.name); // ‘zs’console.log(p2.age); // 19console.log(p3.sex); // ‘male’之前我们就说过,这样做会产生一个问题,就是替换的对象会重新开辟一个新的空间。替换式原型继承时的bug替换原型对象的方式会导致原型的constructor的丢失,constructor属性是默认原型对象指向构造函数的,就算是替换了默认原型对象,这个属性依旧是默认原型对象指向构造函数的,所以新的原型对象是没有这个属性的。解决方法:手动关联一个constructor属性function Person() { this.weight = 50;}var obj = { name: ‘zs’, age: 19, sex: ‘male’}// 在替换原型对象函数之前 给需要替换的对象添加一个 constructor 属性 指向原本的构造函数obj.constructor = Person;// 将一个构造函数的原型对象替换成另一个对象Person.prototype = obj;var p1 = new Person();console.log(p1.proto.constructor === Person); // true4、Object.create()方法实现原型继承当我们想把对象1作为对象2的原型的时候,就可以实现对象2继承对象1。前面我们了解了一个属性:proto,实例出来的对象可以通过这个属性访问到它的原型,但是这个属性只适合开发调试时使用,并不能直接去替换原型对象。所以这里介绍一个新的方法:Object.create()。语法: var obj1 = Object.create(原型对象);示例代码: 让空对象obj1继承对象obj的属性和方法var obj = { name : ‘盖伦’, age : 25, skill : function(){ console.log(‘大宝剑’); }}// 这个方法会帮我们创建一个原型是 obj 的对象var obj1 = Object.create(obj);console.log(obj1.name); // “盖伦"obj1.skill(); // “大宝剑"兼容性:由于这个属性是ECMAScript5的时候提出来的,所以存在兼容性问题。利用浏览器的能力检测,如果存在Object.create则使用,如果不存在的话,就创建构造函数来实现原型继承。// 封装一个能力检测函数function create(obj){ // 判断,如果浏览器有 Object.create 方法的时候 if(Object.create){ return Object.create(obj); }else{ // 创建构造函数 Fun function Fun(){}; Fun.prototype = obj; return new Fun(); }}var hero = { name: ‘盖伦’, age: 25, skill: function () { console.log(‘大宝剑’); }}var hero1 = create(hero);console.log(hero1.name); // “盖伦"console.log(hero1.proto == hero); // true2.原型链对象有原型,原型本身又是一个对象,所以原型也有原型,这样就会形成一个链式结构的原型链。2.1 什么是原型链示例代码: 原型继承练习// 创建一个 Animal 构造函数function Animal() { this.weight = 50; this.eat = function() { console.log(‘蜂蜜蜂蜜’); }}// 实例化一个 animal 对象var animal = new Animal();// 创建一个 Preson 构造函数function Person() { this.name = ‘zs’; this.tool = function() { console.log(‘菜刀’); }}// 让 Person 继承 animal (替换原型对象)Person.prototype = animal;// 实例化一个 p 对象 var p = new Person();// 创建一个 Student 构造函数function Student() { this.score = 100; this.clickCode = function() { console.log(‘啪啪啪’); }}// 让 Student 继承 p (替换原型对象)Student.prototype = p;//实例化一个 student 对象var student = new Student();console.log(student); // 打印 {score:100,clickCode:fn}// 因为是一级级继承下来的 所以最上层的 Animate 里的属性也是被继承的console.log(student.weight); // 50student.eat(); // 蜂蜜蜂蜜student.tool(); // 菜刀如图所示:我们将上面的案例通过画图的方式展现出来后就一目了然了,实例对象animal直接替换了构造函数Person的原型,以此类推,这样就会形成一个链式结构的原型链。完整的原型链结合上图,我们发现,最初的构造函数Animal创建的同时,会创建出一个原型,此时的原型是一个空的对象。结合原型链的概念:“原型本身又是一个对象,所以原型也有原型”,那么这个空对象往上还能找出它的原型或者构造函数吗?我们如何创建一个空对象? 1、字面量:{};2、构造函数:new Object()。我们可以简单的理解为,这个空的对象就是,构造函数Object的实例对象。所以,这个空对象往上面找是能找到它的原型和构造函数的。// 创建一个 Animal 构造函数function Animal() { this.weight = 50; this.eat = function() { console.log(‘蜂蜜蜂蜜’); }}// 实例化一个 animal 对象var animal = new Animal();console.log(animal.proto); // {}console.log(animal.proto.proto); // {}console.log(animal.proto.proto.constructor); // function Object(){}console.log(animal.proto.proto.proto); // null如图所示:2.2 原型链的拓展1、描述出数组[]的原型链结构// 创建一个数组var arr = new Array();// 我们可以看到这个数组是构造函数 Array 的实例对象,所以他的原型应该是:console.log(Array.prototype); // 打印出来还是一个空数组// 我们可以继续往上找 console.log(Array.prototype.proto); // 空对象// 继续console.log(Array.prototype.proto.proto) // null如图所示:2、扩展内置对象给js原有的内置对象,添加新的功能。注意:这里不能直接给内置对象的原型添加方法,因为在开发的时候,大家都会使用到这些内置对象,假如大家都是给内置对象的原型添加方法,就会出现问题。错误的做法:// 第一个开发人员给 Array 原型添加了一个 say 方法Array.prototype.say = function(){ console.log(‘哈哈哈’);}// 第二个开发人员也给 Array 原型添加了一个 say 方法Array.prototype.say = function(){ console.log(‘啪啪啪’);}var arr = new Array();arr.say(); // 打印 “啪啪啪” 前面写的会被覆盖为了避免出现这样的问题,只需自己定义一个构造函数,并且让这个构造函数继承数组的方法即可,再去添加新的方法。// 创建一个数组对象 这个数组对象继承了所有数组中的方法var arr = new Array();// 创建一个属于自己的构造函数function MyArray(){}// 只需要将自己创建的构造函数的原型替换成 数组对象,就能继承数组的所有方法MyArray.prototype = arr;// 现在可以单独的给自己创建的构造函数的原型添加自己的方法MyArray.prototype.say = function(){ console.log(‘这是我自己添加的say方法’);}var arr1 = new MyArray();arr1.push(1); // 创建的 arr1 对象可以使用数组的方法arr1.say(); // 也可以使用自己添加的方法 打印“这是我自己添加的say方法”console.log(arr1); // [1]2.3 属性的搜索原则当通过对象名.属性名获取属性是 ,会遵循以下属性搜索的原则:1-首先去对象自身属性中找,如果找到直接使用,2-如果没找到去自己的原型中找,如果找到直接使用,3-如果没找到,去原型的原型中继续找,找到直接使用,4-如果没有会沿着原型不断向上查找,直到找到null为止。 ...

November 30, 2018 · 4 min · jiezi