原型/原型链/构造函数/实例/继承
1,为什么需要原型?
用构造函数生成实例对象,有一个缺点,无法共享属性和方法。
每一个实例对象,都有自己的属性和方法的副本。这不仅无法做到数据共享,也是极大的资源浪费
考虑到这一点,设计js作者决定为构造函数设置一个prototype属性。
这个属性包含一个对象(以下简称"prototype对象"),所有实例对象需要共享的属性和方法,都放在这个对象里面;那些不需要共享的属性和方法,就放在构造函数里面。
实例对象一旦创建,将自动引用prototype对象的属性和方法。也就是说,实例对象的属性和方法,分成两种,一种是本地的,另一种是引用的。
2,原型(prototype)
概念:每一个构造函数都有一个prototype属性,指向另一个对象。这个对象的所有属性和方法,都会被构造函数的实例继承。
3,原型的继承方式(两类5种)
1,构造函数绑定 (使用call或apply方法,将父对象的构造函数绑定在子对象上,即在子对象构造函数中加一行)
2, prototype模式
3, 直接继承prototype
4,利用空对象作为中介
5,拷贝继承
4,原型链
概念:在javascript中,每个对象都有一个指向它的原型(prototype)对象的内部链接。每个原型对象又有自己的原型,直到某个对象的原型为null为止,组成这条链的最后一环。
有几种方式可以实现继承
1、原型链继承
构造函数、原型和实例之间的关系:每个构造函数都有一个原型对象,原型对象都包含一个指向构造函数的指针,而实例都包含一个原型对象的指针。
继承的本质就是复制,即重写原型对象,代之以一个新类型的实例。
缺点:多个实例对引用类型的操作会被篡改。
function Father(){
this.name = 'Father'
}
Father.prototype.SayFather = function(){
console.log("Father:", this.name) ;
}
function Son(){
//这样设置 会篡改 被继承者的 name 属性,类似 浅拷贝一样,这也是原型链继承的缺点
this.name = 'Son' ;
}
// 创建Father的实例,并将该实例赋值给Son.prototype
Son.prototype = new Father() ;
Son.prototype.SaySon = function(){
console.log("Son:", this.name) //Son:Son
}
let son = new Son() ;
Son.SayFather() ; // Father: Son ,原来为Father: Father,但被继承者给篡改了
Son.SaySon( ) ; // Son:Son
Son.name // Son
2、借用构造函数继承
使用父类的构造函数来增强子类实例,等同于复制父类的实例给子类(不使用原型)
缺点: - 只能继承父类的实例属性和方法,不能实现继承原型属性和方法 - 无法实现复用,每个子类都有父类实例函数的副本,影响性能
function Father(){
this.info = ['A','B','C'] ;
}
Father.prototype.SayFather = function(){} ;
function Son(){
Father.call(this) ; //核心代码
}
let father = new Father(),
son = new Son() ;
son.SayFather() ; //报错,TypeError: son.SayFather is not a function
son.info.push( 'D') ;
son.info // ['A','B','C','D'] ;
father.info // ['A','B','C'] ;//并没有被篡改
3、组合继承
将 原型链和构造函数继承 组合,实现原型链继承 原型属性和方法,构造函数 继承 实例属性和方法
缺点: 在使用子类创建实例对象时,其原型中会存在两份相同的属性/方法。
function Father(){
this.name = 'Father';
}
Father.prototype.SayFather = function () {
console.log("Father: ",this.name) ;
} ;
function Son() {
Father.call(this) ;
this.name = "Son" ; //属性重构, 不会篡改 父类属性
this.age = 18 ;
}
Son.prototype = new Father() ; //原型链继承 属性 会增加 到2份
Son.prototype.constructor = Son ; //指向自己,
Son.prototype.SayFather = function () { //方法重构, 不会篡改 父类 方法
console.log("Son Reload: ",this.name) ;
} ;
let father = new Father() ,
son = new Son() ;
father.SayFather() ; //Father: Father
son.SayFather() ; //Son Reload: Son
4、 寄生组合事继承 (最成熟方法)
Object.create() MDN
-
Object.create()
: 方法创建一个新对象,使用现有的对象来提供新创建的对象的proto。
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
5、混入方法继承多个对象
function Son(){
SuperClass.call( this ) ;
OtherClass.call( this ) ;
}
Son.prototype = Object.create( SuperClass.prototype ) ;
Object.assigin( MyClass.prototype, OtherClass.prototype) :
Son.prototype.constructor = Son ;
...
Object.assign会把 OtherSuperClass原型上的函数拷贝到 Son原型上,使 Son 的所有实例都可用 OtherSuperClass 的方法。
6、ES6类继承 extends
super() 关键词
class MyClass{
constructor( height, width ){
this.height = height ;
this.width = width ;
}
show(){
}
}
class OtherClass extends{
constructor( height, width, name){
super(height, width) ;
this.name = name
}
}
let c = new OtherClass(18,19,'CC') ;
height: 18
name: "CC"
width: 19
__proto__: MyClass
constructor: class OtherClass
__proto__:
constructor: class MyClass
sayHello: ƒ sayHello()
__proto__: Object
用原型实现继承有什么缺点,怎么解决
优点:
可以继承构造函数属性,也可继承原型属性
缺点:
1,在创建子类实例化时,不能向超类型的构造函数中传参
2,子类型继承了父类型原型中的所有属性和方法,但对于引用类型属性值所有实例共享,故不能在不改变其他实例情况下改变。
解决方式
寄生组合事继承(见上题答案中)
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 // jon
jon.age // 18
let flash = new Programmer(22, 'flash')
flash.age // 22
flash.name // flash
jon instanceof Person // true
jon instanceof Programmer // true
flash instanceof Person // true
flash instanceof Programmer // true
优点:不用手动设置原型。
缺点:新语法,只要部分浏览器支持,需要转为 ES5 代码。
arguments
特点
- arguments对象和Function是分不开的。
- 因为arguments这个对象不能显式创建。
- arguments对象只有函数开始时才可用。
使用方法
- 虽然arguments对象并不是一个数组(类数组),但是访问单个参数的方式与访问数组元素的方式相同
arguments[0],arguments[1],。。。arguments[n]; 在js中 不需要明确指出参数名,就能访问它们
function test() {
var s = "";
for (var i = 0; i < arguments.length; i++) {
alert(arguments[i]);
s += arguments[i] + ",";
}
return s;
}
test("name", "age");
输出结果:
name,age
arguments此对象大多用来针对同个方法多处调用并且传递参数个数不一样时进行使用。根据arguments的索引来判断执行的方法。
传多个参数时可以直接用argument,比如求最大值:
function max() {
var max = arguments[0];
console.log(arguments)
for (val of arguments) {
if (val >= max) {
max = val;
}
}
return max;
}
var maxValue = max('9', 1, 2, 4)
console.log(maxValue)
数据类型判断
typeof
返回数据类型,包含这7种: number、boolean、symbol、string、object、undefined、function。typeof null 返回类型错误,返回object
引用类型,除了function返回function类型外,其他均返回object。
其中,null 有属于自己的数据类型 Null , 引用类型中的 数组、日期、正则 也都有属于自己的具体类型,而 typeof 对于这些类型的处理,只返回了处于其原型链最顶端的 Object 类型,没有错,但不是我们想要的结果。toString 这个是最完美的
toString() 是 Object 的原型方法,调用该方法,默认返回当前对象的
[[Class]] 。这是一个内部属性,其格式为 [object Xxx] ,其中 Xxx 就是对象的类型。
对于 Object 对象,直接调用 toString() 就能返回 [object Object] 。而对于其他对象,则需要通过 call / apply 来调用才能返回正确的类型信息。constructor
constructor是原型prototype的一个属性,当函数被定义时候,js引擎会为函数添加原型prototype,并且这个prototype中constructor属性指向函数引用, 因此重写prototype会丢失原来的constructor。
不过这种方法有问题:
1:null 和 undefined 无constructor,这种方法判断不了。
2:还有,如果自定义对象,开发者重写prototype之后,原有的constructor会丢失,因此,为了规范开发,在重写对象原型时一般都需要重新给 constructor 赋值,以保证对象实例的类型不被篡改。instanceof
instanceof 是用来判断 A 是否为 B 的实例,表达式为:A instanceof B,如果 A 是 B 的实例,则返回 true,否则返回 false。 在这里需要特别注意的是:instanceof 检测的是原型,instanceof 只能用来判断两个对象是否属于实例关系, 而不能判断一个对象实例具体属于哪种类型。
作用域链、闭包、作用域
- 作用域
全局作用域:
最外层函数定义的变量拥有全局作用域,即对任何内部函数来说,都是可以访问的
局部作用域:
和全局作用域相反,局部作用域一般只在固定的代码片段内可访问到,而对于函数外部是无法访问的,最常见的例如函数内部.
需要注意的是,函数内部声明变量的时候,一定要使用var命令。如果不用的话,你实际上声明了一个全局变量!
只要函数内定义了一个局部变量,函数在解析的时候都会将这个变量“提前声明” - 作用域链
根据在内部函数可以访问外部函数变量的这种机制,用链式查找决定哪些数据能被内部函数访问。
js为每一个执行环境关联了一个变量对象。环境中定义的所有变量和函数都保存在这个对象中。全局执行环境是最外围的执行环境,全局执行环境被认为是window对象,因此所有的全局变量和函数都作为window对象的属性和方法创建的。
js的执行顺序是根据函数的调用来决定的,当一个函数被调用时,该函数环境的变量对象就被压入一个环境栈中。而在函数执行之后,栈将该函数的变量对象弹出,把控制权交给之前的执行环境变量对象。当某个函数第一次被调用时,就会创建一个执行环境(execution context)以及相应的作用域链,并把作用域链赋值给一个特殊的内部属性([scope])。然后使用this,arguments(arguments在全局环境中不存在)和其他命名参数的值来初始化函数的活动对象(activation object)。当前执行环境的变量对象始终在作用域链的第0位。当某个环境中的所有代码执行完毕后,该环境被销毁(弹出环境栈),保存在其中的所有变量和函数也随之销毁(全局执行环境变量直到应用程序退出,如网页关闭才会被销毁) - 闭包
内部函数的作用域链仍然保持着对父函数活动对象的引用,就是闭包(closure)
闭包有两个作用:
第一个就是可以读取自身函数外部的变量(沿着作用域链寻找)
第二个就是让这些外部变量始终保存在内存中
js函数内的变量值不是在编译的时候就确定的,而是等在运行时期再去寻找的。
使用闭包的注意点
1)由于闭包会使得函数中的变量都被保存在内存中,内存消耗很大,所以不能滥用闭包,否则会造成网页的性能问题,在IE中可能导致内存泄露。解决方法是,在退出函数之前,将不使用的局部变量全部删除。
2)闭包会在父函数外部,改变父函数内部变量的值。所以,如果你把父函数当作对象(object)使用,把闭包当作它的公用方法(Public Method),把内部变量当作它的私有属性(private value),这时一定要小心,不要随便改变父函数内部变量的值。
Ajax的原生写法
Ajax:(Asynchronous JavaScript And XML)指异步 JavaScript 及 XML
JavaScript:更新局部的网页
XML:一般用于请求数据和响应数据的封装
XMLHttpRequest对象:发送请求到服务器并获得返回结果
CSS:美化页面样式
异步:发送请求后不等返回结果,由回调函数处理结果
var Ajax = {
get: function(url, fn) {
//创建XMLHttpRequest对象
var xhr = new XMLHttpRequest();
//true表示异步
xhr.open('GET', url, true);
xhr.onreadystatechange = function() {
// readyState == 4说明请求已完成
if(xhr.readyState == 4 && xhr.status == 200 || xhr.status == 304) {
//responseText:从服务器获得数据
fn.call(this, xhr.responseText);
}
};
xhr.send();
},
post: function(url, data, fn) { //datat应为'a=a1&b=b1'这种字符串格式
var xhr = new XMLHttpRequest();
xhr.open("POST", url, true);
// 添加http头,发送信息至服务器时内容编码类型
xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
xhr.onreadystatechange = function() {
if(xhr.readyState == 4 && (xhr.status == 200 || xhr.status == 304)) {
fn.call(this, xhr.responseText);
}
};
xhr.send(data);
}
}
对象深拷贝、浅拷贝
浅拷贝和深拷贝的'深浅'主要针对的是对象的‘深度’,常见的对象都是'浅'的,也就是对象里的属性就是单个的属性,而'深'的对象是指一个对象的属性是另一个对象,也就是对象里面嵌套对象,就像嵌套函数一样。
浅拷贝中,原始值和副本共享同样的属性。
浅拷贝只拷贝了对象引用。
浅拷贝中如果修改了拷贝对象会影响到原始对象,反之亦然。
js中,数组和对象的赋值默认为浅拷贝。
深拷贝指递归的复制对象的属性给新对象。jquery中我们使用$.extend去进行深拷贝。
//浅拷贝1
let obj01 = {
name: 'Lily',
age: '20',
time: ['13', '15'],
person: {
name: 'Henry',
age: '21'
}
};
let obj02 = obj01;
obj02.age = '25'; //会改变obj11的age
obj02.person.age = '25'; //会改变obj11的person.age
obj02.time[1] = '25'; //会改变obj11的time值
console.log(obj01);
深拷贝中,副本和原对象不共享属性
深拷贝递归的复制属性
深拷贝的副本不会影响到原对象,反之亦然
js中所有的原始数据类型默认执行深拷贝,比如Boolean, null, Undefined, Number,String等
//深拷贝方法1-JSON.parse(JSON.stringify(obj))
let obj21 = {
name: 'Lily',
age: '20',
person: {
name: 'Henry',
age: '21'
}
};
let obj22 = JSON.parse(JSON.stringify(obj21));
obj22.person.age = '25'; //不会改变obj31的person.age
console.log(obj21);
//深拷贝方法2-迭代递归法for...in
let obj31 = {
name: 'Lily',
age: '20',
time: ['13', '15'],
person: {
name: 'Henry',
age: '21'
}
};
function deepObject(obj){ //深拷贝
let cloneObj = {};
for(let key in obj){
let objChild = Object.prototype.toString.call(obj[key]);
cloneObj[key] = (objChild === '[object Array]' || objChild === '[object Object]') ? deepObject(obj[key]) : obj[key];
}
return cloneObj;
}
let obj32 = deepObject(obj31);
obj32.time[0] = '25'; //不会改变obj31的time值
obj32.person.age = '25'; //不会改变obj31的person.age
console.log(obj31);
图片懒加载、预加载
- 懒加载也叫延迟加载:JS图片延迟加载 延迟加载图片或符合某些条件时才加载某些图片。
- 预加载:提前加载图片,当用户需要查看时可直接从本地缓存中渲染。
两种技术的本质:两者的行为是相反的,一个是提前加载,一个是迟缓甚至不加载。懒加载对服务器前端有一定的缓解压力作用,预加载则会增加服务器前端压力。
懒加载的意义及实现方式有:
懒加载的意义: 懒加载的主要目的是作为服务器前端的优化,减少请求数或延迟请求数。
实现方式:
1.第一种是纯粹的延迟加载,使用setTimeOut或setInterval进行加载延迟.
2.第二种是条件加载,符合某些条件,或触发了某些事件才开始异步下载。
3.第三种是可视区加载,即仅加载用户可以看到的区域,这个主要由监控滚动条来实现,一般会在距用户看到某图片前一定距离遍开始加载,这样能保证用户拉下时正好能看到图片。
预加载的意义及实现方式有:
预加载可以说是牺牲服务器前端性能,换取更好的用户体验,这样可以使用户的操作得到最快的反映。实现预载的方法非常多,可以用CSS(background)、JS(Image)、HTML(<img />)都可以。常用的是new Image();,设置其src来实现预载,再使用onload方法回调预载完成事件。只要浏览器把图片下载到本地,同样的src就会使用缓存,这是最基本也是最实用的预载方法。当Image下载完图片头后,会得到宽和高,因此可以在预载前得到图片的大小(方法是用记时器轮循宽高变化)。
实现页面加载进度条
document.onreadystatechange=function () {
if ("interactive"==document.readyState){
var ahtml="<div class='loading'> <div class='pic'> <img src='images/loading.svg' width='80px' height='80px' ><br/><b >"+jjj+"</b></div></div>" ;
$("body").append(ahtml);
}else
if ("complete"==document.readyState){
$(".loading").fadeOut(100);
}
};
this关键字
this是JavaScript的一个关键字,表示的不是对象本身,而是指被调用的上文。
- 主要用于以下四种环境:
1.直接调用,表示的是全局对象,window;
调用函数的结果是1,即this指的是全局对象,window,所以count值为全局变量的值;
var count = 1;
function func(){
console.log(this.count);
}
func(); //1
2.作为对象方法被调用,表示的是该对象
函数fn()方法作为对象的方法被调用,此时this表示的被调用的对象obj,所以count值为obj对象中的值count,若是对象obj中没有count属性,则为undefined
var count = 1;
function func(){
console.log(this.count);
}
var obj = {};
obj.count = 2;
obj.show = func;
obj.show(); //2
3.作为构造函数被调用,表示的是创建的实例;
func()作为构造函数,创建了他的实例,此时this表示的是创建的实例Func
var count = 1;
function func(){
this.count = 2;
}
var Func = new func();
console.log(count); //1
console.log(Func.count); //2
4.可以使用apply(),call()改变this的表示对象,第一个参数就是this.
apply()是函数对象的一个方法,它的作用是改变函数的调用对象(实则是将某某对象的某个方法放到另一个好基友对象那里去执行),它的第一个参数就表示改变后的调用这个函数的对象。因此,this指的就是这第一个参数。同call()
可以使用apply()或是call()来改变this指向。此时this表示的是obj对象
var count = 1;
function func(){
console.log(this.count);
}
var obj = {};
obj.count = 2;
func.call(); //1
func.call(obj); //2
5.嵌套函数作用域中的this:
嵌套函数被调用时并没有继承被嵌套函数的this引用,在嵌套函数被调用时,this指向全局对象。在有些应用中,我们需要在嵌套函数中读取调用被嵌套函数的对象的属性,此时可以声明一个局部变量保存this引用,
var count = 1;
function func(){
console.log(this.count);
function func2(){
console.log(this.count);
}
func2();
}
var obj = {};
obj.count = 2;
obj.show = func;
obj.show(); //2,1
此时,嵌套函数与被嵌套函数中的this都表示了obj对象。
var count = 1;
function func(){
console.log(this.count);
var self = this;
function func2(){
console.log(self.count);
}
func2();
}
var obj = {};
obj.count = 2;
obj.show = func;
obj.show(); //2,2
手动实现parseInt
手写parseInt的实现:要求简单一些,把字符串型的数字转化为真正的数字即可,但不能使用JS原生的字符串转数字的API,比如Number()
function _parseInt(str, radix) {
let str_type = typeof str;
let res = 0;
if (str_type !== 'string' && str_type !== 'number') {
// 如果类型不是 string 或 number 类型返回NaN
return NaN
}
// 字符串处理
str = String(str).trim().split('.')[0]
let length = str.length;
if (!length) {
// 如果为空则返回 NaN
return NaN
}
if (!radix) {
// 如果 radix 为0 null undefined
// 则转化为 10
radix = 10;
}
if (typeof radix !== 'number' || radix < 2 || radix > 36) {
return NaN
}
for (let i = 0; i < length; i++) {
let arr = str.split('').reverse().join('');
res += Math.floor(arr[i]) * Math.pow(radix, i)
}
return res;
}
为什么会有同源策略
同源策略是为了保护网站的安全,防止用户信息泄露,防止身份伪造等(读取Cookie)
跨域并不是请求发不出去,请求能发出去,服务端能收到请求并正常返回结果,只是结果被浏览器拦截了,之所以会跨域。
- 不同源会怎么样?主要表现在3点
(1) 无法用js读取非同源的Cookie、LocalStorage 和 IndexDB 无法读取。
(2) 无法用js获取非同源的DOM。
(3) 无法用js发送非同源的AJAX请求。更准确的说,js可以向非同源的服务器发请求,但是服务器返回的数据会被浏览器拦截。 - 是谁来执行同源策略的?
同源策略是由浏览器来执行。所有的限制都是浏览器的作用。这是浏览器为了保护用户的数据安全而采取的策略。
同源策略如何保护用户数据安全?没有同源策略会怎么样?
同源策略对于js的限制有3点,我们一点一点来说。
(1) 无法用js读取非同源的Cookie、LocalStorage 和 IndexDB 无法读取。
这条很好理解。
为了防止恶意网站通过js获取用户其他网站的cookie。
(2) 无法用js获取非同源的DOM 。
如果没有这一条,恶意网站可以通过iframe打开银行页面,可以获取dom就相当于可以获取整个银行页面的信息。
怎么判断两个对象是否相等
两个Object类型对象,即使拥有相同属性、相同值,当使用 == 或 === 进行比较时,也不认为他们相等。这就是因为他们是通过引用(内存里的位置)比较的,不像基本类型是通过值比较的。
先判断俩者是不是对象
是对象后俩者长度是否一致
判断俩个对象的所有key值是否相等相同
判断俩个对象的相应的key对应的值是否相同
来一个递归判断里面的对象循环1-4步骤
function diff(obj1,obj2){
var o1 = obj1 instanceof Object;
var o2 = obj2 instanceof Object;
// 判断是不是对象
if (!o1 || !o2) {
return obj1 === obj2;
}
//Object.keys() 返回一个由对象的自身可枚举属性(key值)组成的数组,
//例如:数组返回下表:let arr = ["a", "b", "c"];console.log(Object.keys(arr))->0,1,2;
if (Object.keys(obj1).length !== Object.keys(obj2).length) {
return false;
}
for (var o in obj1) {
var t1 = obj1[o] instanceof Object;
var t2 = obj2[o] instanceof Object;
if (t1 && t2) {
return diff(obj1[o], obj2[o]);
} else if (obj1[o] !== obj2[o]) {
return false;
}
}
return true;
}
其实还有一种比较简单的对象是否相等的判断方式
JSON.stringify(obj)==JSON.stringify(obj);//true