最近在从新看js基础,索性就将继承、闭包、原型链这三个原生js中比较重要的点写篇文章总结一下。本身明白理解是一回事,写了文章让别人看明白是另一回事,经过讲述,本身也能进步。
JS
的做者Brendan Eich
在设计这门编程语言时,只是为了让这门语言做为浏览器与网页互动的工具。他以为这门语言只须要能完成一些简单操做就够了,好比判断用户是否填写了表单。
基于简易语言的设计初衷,做者以为JS
不须要有相似java
等面向对象语言所拥有的“继承”机制。可是考虑到JS
中一切皆对象(全部的数据类型均可以用对象来表示),必须有一种机制,把全部的对象联系起来,实现相似的“继承”机制。
不一样于大部分面向对象语言,ES6
以前并无引入类(class
)的概念,JS
并不是经过类而是经过构造函数来建立实例,使用prototype
原型模型来实现“继承”。
在 JavaScript
里,构造函数一般是用来实现实例的,JavaScript
没有类的概念,可是有特殊的构造函数。构造函数本质上是个普通函数,充当类的角色,主要用来建立实例,并初始化实例,即为实例成员变量赋初始值。java
构造函数和普通函数的区别在于,构造函数应该遵循如下几点规范:golang
new
关键字来进行调用;this
指向的是新建立的实例;return
表达式,通常状况下,会隐式地返回 this
,也就是新建立的对象,若是想要使用显式的返回值,则显式的返回值必须是对象,不然依然返回实例。构造函数是用来建立实例的
// 步骤1:新建构造函数 function Person(name) { this.name = name; this.sayName = function() { console.log(this.name); } } // 步骤2:建立实例 var person = new Person('yang');
此时,以下图所示,针对步骤1,当构造函数被建立时,会在内存空间新建一个对象,构造函数内有一个属性 prototype
会指向这个对象的存储空间,这个对象称为构造函数的原型对象。
针对步骤2,以下图所示,person
是经过 Person
构造函数建立的实例,在 person
内部将包含一个指针(内部属性),指向构造函数的原型对象,这个指针称为 [[prototype]]
。
目前,大部分浏览器都支持 __proto__
这个属性来访问构造函数的原型对象,就像这里,person.__proto__
指向 Person.prototype
的对象存储空间。
由上面示例图知道,实例 person
若是访问原型对象,须要使用 __proto__
这个属性。
事实上,__proto__
是一个访问器属性(由一个 getter
函数和一个 setter
函数构成),但做为访问 [[prototype]]
的属性,它是一个不被推荐的属性, JavaScript
规范中规定,这个属性仅在浏览器环境下才能使用。[[prototype]]
是内部的并且是隐藏的,当须要访问内部 [[prototype]]
时,可使用如下现代方法:编程
// 返回对象 `obj` 的 `[[prototype]]`。 Object.getPrototypeOf(obj); // 将对象 `obj` 的 `[[prototype]]` 设置为 `proto`。 Object.setPrototypeOf(obj, proto) // 利用给定的 `proto` 做为 `[[prototype]]` 和属性描述符(可选)来建立一个空对象。 Object.create(proto[, descriptors])
在默认状况下,全部的原型对象都会自动得到一个 constructor
的属性,这个属性包含一个指向 prototype
所在函数的指针,即 constructor
属性会指向构造函数自己。
此外,Person.prototype
指向的位置是一个对象,也包含有内部 [[prototype]]
指针,这个指针指向的是 Object.prototype
,是一个对象。这个关系表示,Person.prototype
是由 Object
做为构造函数建立的。
须要注意的是,原型是能够被改写的。可是 JavaScript
中对其作了规定,只能够被改写成对象,若是改写成其余值(空值 null
也不行),会自动被忽略,会让原型链下一级来替换这个被改写的原型。浏览器
- 属性公用化:原型能够存储一些默认属性和方法,而且在各个不一样的实例中能够共享使用;
- 继承:在子类构造函数中借用父类构造函数,再经过原型来继承父类的原型属性和方法,模拟继承的效果;
- 节省存储空间:结合第1点,公用的属性和方法多了,对应须要的存储空间也减小了。
// 第一步 新建构造函数 function Person(name) { this.name = name; this.age = 18; this.sayName = function() { console.log(this.name); } } // 第二步 建立实例 1 var person1 = new Person('1号'); // 第三步 建立实例2 var person2 = new Person('2号'); // 结果均为 true person1.__proto__ === Person.prototype; person2.__proto__ === Person.prototype; // 1号 2号 console.log(person1.name, person2.name); // 18 18 console.log(person1.age, person2.age);
JavaScript
中,万物皆对象(全部的数据类型均可以用对象来表示),对象与对象之间存在关系,并非孤立存在的,对象之间的继承关系,在JavaScript
中实例对象经过内部属性[[prototype]]
指向父类对象的原型空间,直到指向浏览器实现的内部对象Object
为止,Object
的内部属性[[prototype]]
为null
,这样就造成了一个原型指向的链条,这个链条称为原型链。
当访问对象的属性时,会先在对象自身属性中查找,若是有则直接返回使用,若是没有则会顺着原型链指向继续寻找(不断查找内部属性 [[prototype]]),直到寻找浏览器内置对象的原型,若是依然没有找到,则返回 undefined。
须要注意的是,原型链中访问器属性和数据属性在读写上是有区别的(点击了解访问器属性和数据属性)。若是在原型链上某一级设置了访问器属性(假设为 age
),则读取 age
时,直接按访问器属性设置的值返回;写入时也是以访问器属性为最优先级。在数据属性的读写上,读取时,会按照原型链属性查找进行查找;写入时,直接写入当前对象,若原型链中有相同属性,会被覆盖。
能够结合如下代码来对原型链进行分析:网络
// 第一步 新建构造函数 function Person(name) { this.name = name; this.age = 18; this.sayName = function() { console.log(this.name); } } // 第二步 建立实例 var person = new Person('person'); 复制代码
根据以上代码,能够获得下面的图示:闭包
第一步中,新建 Person
的构造函数,此时原型空间被建立;第二步中,经过 new
构造函数生成实例 person
,person
的 [[prototype]]
会指向原型空间。app
不少人容易忽视的是浏览器对于下面的处理,这里 Person.prototype.__proto__
指向内置对象,由于 Person.prototype
是个对象,默认是由 Object
函数做为类建立的,而 Object.prototype
为内置对象。异步
而 Person.__proto__
指向内置匿名函数 anonymous
,由于 Person
是个函数对象,默认由 Function
做为类建立,而 Function.prototype
为内置匿名函数 anonymous
。编程语言
这里还须要注意一个点,Function.prototype
和 Function.__proto__
同时指向内置匿名函数 anonymous
,这样原型链的终点就是 null
,而不用担忧原型链查找会陷入死循环中。函数
一、 借助call
function Parents(age, live) { this.name = '借助call方式实现继承' this.age = age this.live = live } function Child() { Parents.call(this, ...arguments) } let child = new Child(18, true) console.log('child: ', child)
缺点:这样写的时候子类虽然可以拿到父类的属性值, 可是问题是父类原型对象中一旦存在方法那么子类没法继承。
二、借助原型链
function Parents1(age) { this.name = "借助原型链实现继承" this.age = age } function Child1() { this.type = 'Child1' } Child1.prototype = new Parents1() let child1 = new Child1() console.log("child1: ", child1.name)
缺点:改变实例的属性会影响到父类的属性,由于共用一个原型对象(引用类型)
三、 将前两中组合(组合式继承)
function Parents2(age) { this.name = '借助组合式实现继承' this.age = age this.arr = [1, 2, 3] } function Child2() { this.type = 'Child2' Parents2.call(this, ...arguments) } Child2.prototype = new Parents2() let child2 = new Child2(12) let anthorChild2 = new Child2(13) child2.arr.push(4) console.log('child2: ', child2) console.log('anthorChild2: ', anthorChild2)
缺点:这种继承的问题 那就是Parent2的构造函数会多执行了一次(Child2.prototype = new Parent2();)
四、组合继承的优化
function Parents3(age) { this.age = age this.name = '组合继承的优化1' } function Child3() { Parents.call(this, ...arguments) this.type = 'Child3' } // 这里让将父类原型对象直接给到子类,父类构造函数只执行一次, // 并且父类属性和方法均能访问 Child2.prototype = Parents3.prototype
缺点:子类实例的构造函数是Parent3,显然这是不对的,应该是Child3。
五、寄生组合式继承
function Parents4(age) { this.age = age this.name = '寄生组合式继承' } function Child4() { Parents.apply(this, [...arguments]) this.type = 'Child4' } Child4.prototype = Object.create(Parents4.prototype) Child4.prototype.constructor = Child4
这是最推荐的一种方式, 接近完美的继承, 它的名字也叫作寄生组合继承。
六、ES6的extends
它用的就是寄生组合式继承,可是加了一个Object.setPrototypeOf(subClass, superClass)
是用来继承父类的静态方法。这也是原来的继承方式疏忽掉的地方。
扩展:面向对象继承的问题,没法决定继承哪些属性, 全部属性都得继承。
例如:不一样的车有不一样的功能
function drive(){ console.log("发动"); } function music() { console.log("音乐") } function addOil() { console.log("加油") } // compose是一个组合各类方法的方法 // 普通汽车 let car = compose(drive, music, addOil); // 新能源 let newEnergyCar = compose(drive, music);
闭包是指有权访问另一个函数做用域中的变量的函数(红宝书)
闭包是指那些可以访问自由变量的函数。(MDN)其中自由变量, 指在函数中使用的, 但既不是函数参数arguments也不是函数的局部变量的变量,其实就是另一个函数做用域中的变量。)
提及闭包,就必需要说说做用域,ES5种只存在两种做用域:一、函数做用域。二、全局做用域
当访问一个变量时, 解释器会首先在当前做用域查找标示符,若是没有找到, 就去父做用域找, 直到找到该变量的标示符或者不在父做用域中, 这就是做用域链,每个子函数都会拷贝上级的做用域, 造成一个做用域的链条。
let a = 1; function f1() { var a = 2 function f2() { var a = 3; console.log(a); //3 } }
在这段代码中, f1的做用域指向有全局做用域(window) 和它自己,而f2的做用域指向全局做用域(window)、 f1和它自己。并且做用域是从最底层向上找, 直到找到全局做用域window为止,若是全局尚未的话就会报错。闭包产生的本质就是, 当前环境中存在指向父级做用域的引用。
function f2() { var a = 2 function f3() { console.log(a); //2 } return f3; } var x = f2(); x();
这里x会拿到父级做用域中的变量, 输出2。 由于在当前环境中,含有对f3的引用, f3偏偏引用了window、 f3和f3的做用域。 所以f3能够访问到f2的做用域的变量。那是否是只有返回函数才算是产生了闭包呢?回到闭包的本质,只须要让父级做用域的引用存在便可。
var f4; function f5() { var a = 2 f4 = function () { console.log(a); } } f5(); f4();
让f5执行,给f4赋值后,等于说如今f4拥有了window、f5和f4自己这几个做用域的访问权,仍是自底向上查找,最近是在f5中找到了a,所以输出2。在这里是外面的变量f4存在着父级做用域的引用, 所以产生了闭包,形式变了,本质没有改变。
var b = 1; function foo() { var b = 2; function baz() { console.log(b); } bar(baz); } function bar(fn) { // 这就是闭包 fn(); } // 输出2,而不是1 foo(); // 如下的闭包保存的仅仅是window和当前做用域。 // 定时器 setTimeout(function timeHandler() { console.log('111'); }, 100) // 事件监听 // document.body.click(function () { // console.log('DOM Listener'); // }) // 当即执行函数 var c = 2; (function IIFE() { // 输出2 console.log(c); })();
for (var i = 1; i <= 5; i++) { setTimeout(function timer() { console.log(i) }, 0) } // 6 6 6 6 6 6 // 为何会所有输出6? 如何改进, 让它输出1, 2, 3, 4, 5?
解析:
// 一、利用IIFE(当即执行函数表达式)当每次for循环时,把此时的i变量传递到定时器中 for (var i = 0; i < 5; i++) { (function (j) { setTimeout(() => { console.log(j) }, 1000); })(i) } // 二、给定时器传入第三个参数, 做为timer函数的第一个函数参数 for (var i = 0; i < 5; i++) { setTimeout(function (j) { console.log(j) }, 1000, i); } // 三、使用ES6中的let // let使JS发生革命性的变化, 让JS有函数做用域变为了块级做用域, // 用let后做用域链不复存在。 代码的做用域以块级为单位, for (let i = 1; i <= 5; i++) { setTimeout(function timer() { console.log(i) }, 2000) }
以上部份内容来源与本身复习时的网络查找,也主要用于我的学习,至关于记事本的存在,暂不列举连接文章。若是有做者看到,能够联系我将原文连接贴出。