JS高级
1、面向对象
面向对象的三大特性:封装,继承,多态;
tips:JS没有多态;
2、封装函数
传统封装函数的方式:
function fun_1(){
}
封装函数会出现的问题:1、全局变量污染;
2、代码结构混乱,不利于维护;
构造函数:
function Person(){
this.name = name;
this.age = age;
this.sayHi = function (){};
}
var p = new Person;
var p1 = new Person;
tips:在这里,如果将方法放在了构造函数内部,
每一次利用构造函数创建对象,都需要开辟一个新空间保存该方法,
并且这些方法都是相同的,这造成了资源的浪费;
tips:因此,我们将创建出来的对象需要用到的方法都放在prototype里;
每一个利用该构造函数创建出来的实例化对象都会指向同一个prototype;
tips:如果构造函数没有return,其调用的值为undefined;
eg:
var pig = new Pig(); ——→这样创建出了一个实例化对象,对象名叫做pig;
var pig2 = Pig(); ——→这样只是把构造函数当函数调用,函数的返回值赋值给pig2;
prototype:
1、构造函数.prototype可以访问到原型;
2、创建出来的实例化对象.__proto__可以访问到原型;
3、原型也可以使用.constructor方法访问到原型的构造函数;
4、实例化.constructor可以访问到构造函数,但是构造函数无法访问到实例化对象;
5、原型里面一般存放的是实例化对象都要公用到的方法;
构造函数里面存放的是对于新创建出来的对象的属性初始化的操作;
tips:尽量将所有的功能都写在原型函数中,便于实例化对象使用;
在构造函数中仅调用必要的功能,且在原型函数内的功能可以进行分类,在构造函数仅执行总类功能,细类封装在原型中;
6、每一个新创建出来的对象,如果调用它的方法,首先将从它本身的属性中寻找该方法;
如果该实例化对象的属性中没有这个方法,将会向其prototype中寻找;
7、当构造函数的原型在中途被替换成了其他对象时,这时候,已经创建的对象的原型不会改变,
但是在这之后创建的对象的原型都会指向这个替换过后的对象;
8、使用对象的动态特性,可以给原型对象添加成员;
eg:
Person.prototype.name = "xxx";
Person.prototype["name"] = "xxx";
tips:两种方法都适用;
tips:一个对象的属性和方法统称为成员;
3、原型图
![在这里输入图片标题 输入图片说明](https://static.oschina.net/uploads/img/201703/27083650_7CaM.jpg)
4、继承
(1)、混入式继承(mix-in)
使用for-in循环,将对象1的属性赋值给对象2;
eg:
for(var key in obj_1){
obj_2[key] = obj_1[key];
}
tips:通过混入式继承只能给单一对象实现继承;
要让创建出的所有对象都拥有某个对象的属性,用混入式将会很麻烦;
(2)、原型继承(prototype)
要让构造函数的原型拥有某个对象的属性, 有两种方法:
1、直接将该构造函数的原型指定为该对象;
2、将该对象的属性循环地添加到原型中,推荐使用该方法;
(3)、经典继承
var obj_2 = Object.create(obj_1);
创建一个obj_2,令obj_1作为obj的原型;
(4)、应用:安全地使用内置对象
如果将需要的方法都放在原型中,原型的结构会变成混乱,
而且所有人都把方法放在原型里面,多人操作会造成方法冲突。
因此需要加入新的原型继承,使之能够安全地使用内置对象;
eg:
var arr = new MyArray();
MyArray.prototype = [];
tips:这样就在arr和内置对象Array的中间插入了一个自定义的原型MyArray;
我们可以将我们需要的方法存放在自定义的原型中, 这样创建的数组对象仍然可以获取来自内置对象Array的方法;
5、Object.prototype成员
(1)、hasOwnProperty("属性名");
eg:
obj.hasOwnProperty(key);
判断obj对象中是否存在属性key;
(2)、isPrototypeOf
eg:
obj_1.isPrototypeOf(obj_2);
判断obj_1是否是obj_2的原型;
(3)、propertyIsEnumerable
eg:
obj.propertyIsEnumerable("属性名");
判断在obj中该属性是否可以通过枚举得到,
tips:这个属性必须是该对象自身拥有的,而非从prototype引用;
(4)、toString()
toLocaleString()
二者都是转换成字符串的方法,LocalString显示为本地区相关的格式;
tips:一个非常明显的区别就是Date对象显示出来的日期;
(5)、valueOf()
array.valueOf()仍然是一个数组;
object.valueOf()仍然是一个对象;
6、function成员
(1)、length:表示函数的形参个数
(2)、name
匿名函数的name是anonymous;
(3)、prototype:
当函数作为构造函数存在的时候,prototype就指向了这个构造函数的原型;
(4)、caller
函数在哪个函数内部调用,那么caller就指向这个外部的函数,
如果是在全局调用的,就是null;
eg:
function Person(a, b){
console.log(Person.caller);
}
function f1(){
Person(1,2,3);
}
f1();
(5)、__proto__:
当函数作为实例化对象存在的时候,__proto__就指向了这个实例化对象的原型;
7、Function对象
(1)、
var fn1 = new Function(arg1, arg2, arg3, ..., argN, body);
body写函数内部的代码,arg1指该函数的形参;
这时候,就创建出了一个名为fn1的函数。
eg:
var func = new Function(){"num", "console.log(num);"};
tips:Function是一个构造函数,这个构造函数既可以看作是构造函数,也可以看作是Function(即自身)的实例化对象;
任何函数的构造函数都是Function;
(2)、instance of
判断构造函数的原型是否在对象的原型链上,返回值为true或false;
eg:
obj instanceof Object; //obj为实例化对象或构造函数
tips:任何对象 instanceof Object,其返回值都为true;
(3)、函数的三种创建方式
eg:
var fn = new Function();
var fn = function (){};
function fn(){}
tips:函数加了括号是调用,没有加括号是赋值;
8、eval函数
(1)、Evaluate,将字符串解析成js代码;
eg:
eval("var a = 10;");
这段字符串使用了eval方法后,将会创建一个变量a,赋值为10;
(2)、将JSON格式的字符串转换成js对象
1、JSON.parse(json)字符串,返回值为js对象,有兼容问题;
2、json2.js插件,引入该插件即可解决;
3、使用eval函数将JSON格式的字符串转换成对象;
tips:使用{}可以进行代码块划分,从而使代码结构更清晰;
eg:
{
var a = 10;
}
在使用eval方法时,为了避免json字符串的首尾的{}解析成了代码块,
eg:
var obj = eval("(" + JSON字符串 + ")");
或
eval("var obj = " + str);
tips:Function 和 eval都可以将字符串转换成代码,但是不推荐使用这种方式,
这种方式具有安全性问题(例如XSS跨站脚本攻击);
9、静态成员和实例成员
(1)、静态成员:通过构造函数去访问的成员,即在new的时候构造函数提供的成员;
(2)、实例成员:通过实例去访问的成员,即实例自身有的属性;
10、arguments对象
arguments对象里面存储了所有传入函数内部的实参;
属性:
(1)、length,可表示传入实参的个数;
(2)、callee,表示当前arguments对象所在的函数;
eg:
function test(a, b){
console.log(arguments.callee);
}
test(1, "a");
tips:这时候在控制台显示的为text()函数的内容;
11、递归
函数自己调用自己,即为递归;
(1)、递归的两个条件
1、在函数内部自己调用了自己;
2、函数具有结束条件,结束条件达到时递归结束;
tips:递归必须有结束条件,没有结束条件的递归是死循环;
(2)、递归的效率极低,十分耗费资源,一般要配合缓存使用;
(3)、eg:
求前n项和:
function sum(n){
if(n == 1){
return 1;
}
return sum(n - 1) + n;
}
tips:在最小的时候出现的值,即n==1的时候,结束条件达成需要return的值;
1+2+3+4
1+2+3
1+2
1
所以n==1的时候return 1;
求n的阶乘:
function fct(n){
if(n == 1){
return 1;
}
return fct(n - 1) * n;
}
tips: 1*2*3*4*5
1*2*3*4
1*2*3
1*2
1
所以当n==1的时候return 1;
12、作用域与变量提升
(1)、在js中,有且只有函数可以创建作用域;
和词法作用域对应的作用关于叫做动态作用域;
js中的作用域是词法作用域;
js不是动态作用域;
tips:若为动态作用域,会考虑到函数的调用环境,来判断其变量提升;
(2)、js执行分为两阶段
1、预解析,变量提升(hoisting),function和var进行提升;
2、代码执行阶段;
tips:在if语句内部的var会进行变量提升,但是赋值操作不会;
eg:
function test(a){
console.log(typeof a);
var a = 123;
}
test(123);
tips:在函数中,传入的实参,会在函数内部进行一个var 形参 = 实参的操作,先于所有的函数代码之前;
这里调用了函数,传入了实参,执行的顺序为:
test(123);
var a = 123;
var a ;
console.log(typeof a);
a = 123;
(3)、在function中出现的变量,若未在函数内部声明,则向上一级的作用域寻找定义了的变量进行赋值。
以此类推,直到找到了该变量的var,一直到全局作用域仍为定义,则报错。
tips:if(undefined),为false;
if(0),为false;
if(null),为false;
if(" "),为false;
tips:元素中的属性,可以直接用点方法进行调用。
如,body.id、body.class。
13、作用域链与变量搜索原则
(1)、绘制作用域链的步骤:
1、看整个全局是一条链, 即顶级链, 记为 0 级链;
2、看全局作用域中, 有什么变量和函数声明, 就以方格的形式绘制到 0 级练上
3、再找函数, 只有函数可以限制作用域, 因此从函数中引入新链, 标记为 1 级链
4、然后在每一个 1 级链中再次往复刚才的行为
(2)、变量的访问规则
1、首先看变量在第几条链上, 在该链上看是否有变量的定义与赋值, 如果有直接使用
2、如果没有到上一级链上找( n - 1 级链 ), 如果有直接用, 停止继续查找.
3、如果还没有再次往上刚找... 直到全局链( 0 级 ), 还没有就是 is not defined
tips:注意,同级的链不可混合查找
(3)、
Foo.get,表示调用了在构造函数Foo中的get属性;
Foo().get,先执行了Foo()函数,调用其返回值的get属性;
tips:函数加了括号则为调用;
tips:构造函数如果当做普通函数来调用,其内部this为window,
return什么就是什么,没有return返回undefined;
new Foo.getName();
直接计算Foo.getName属性的值;
new Foo().getName();
new Foo(),创建了一个Foo的实例化对象,在该对象里面调用其getName方法,如果该对象本身不具备该方法则向原型查找;
new new Foo().getName();
与上例类似;
tips:new和一个(),中间的部分即为构造函数,new本身并不对结果产生影响;
14、闭包
(1)、闭包(Closure),在函数内部创建新的函数,用于修改函数内部定义的变量,
在外部函数return一个内部函数,这样就实现了对函数内部变量的操作;
概念:指可以访问独立变量的函数;
(2)、闭包作用:1、解决全局变量污染的问题;
2、保护变量,可以对变量赋值做一些校验;
(3)、闭包的缺陷:定义的函数因为返回值一直处于被调用状态,因此资源无法被自动释放,内存一直被占用,造成了资源浪费;
如果能不用闭包就尽量不用;
(4)、应用
eg:
for(var i = 0; i < 10; i++){
setTimeout(function(j){
//var j = i
function inner(){
console.log(j);
}
return inner;
}(i), 1000);
}
tips:在js中存在事件队列的说法,for循环的事件队列要优先于setTimeout,
因此如果不用闭包,setTimeout的功能将无法正常进行;
tips:js中的任务分为主要任务和次要任务;
主要任务(for, var ,function ...etc);
次要任务(setTimeout,setInterval ...etc);
eg:
function func(){
var num = Math.random();
var obj = {
setNum: function(value){
num = value;
},
getNum: function(){
return num;
}
}
return obj;
}
tips:在return的时候返回了一个对象,对象里面存储了内部函数,通过调用这些内部函数方法,可以对函数中的变量进行操作;、
15、缓存
浏览器缓存
CDN,Content Delivery Network,内容分发网络;
硬件缓存(RAM,内存);
代码实现缓存:
用一个数组或者对象存储数据,在发生了繁琐重复操作时有限从缓存数组中查找结果,缓存没有结果再作计算;
eg:
利用缓存和闭包,对递归的优化,以斐波那契数列为例。
function createFib(n){
var arr = []; //充当缓存的数组
function fib(n){
var num = arr[n];
//判断在缓存数组里是否有这个数,有则直接用,没有就计算;
if(!num){
if(n == 1 || n == 2){
num = 1;
}else{
num = fib(n - 1) + fib(n - 2);
}
arr[n] = num;
}
return num;
}
return fib(n);
}
createFib(5);
16、沙箱模式
(1)、特征:在沙箱内部的操作,不会对外界产生影响;
(2)、沙箱的基本模型:自调用函数(IIFE,Immediately Invoked Function Expression);
eg:
(function(){
var a = 10;
})()
(3)、自调用函数的多种写法:只要能作为一个句子存在,并进行了自调用即可;
(function (){})();
(function (){}());
+ function (){}();
! function (){}();
(4)、向外暴露接口操作
(function (w){
window.jQuery = window.$ = jQuery;
})(window)
tips: 1、传参的意义在于,实现逻辑上的隔离;
2、代码压缩时,其实就是对变量重命名,但window对象和其他内置对象并不能重命名,
这时候如果直接使用window对象会不利于代码压缩,所以需要自定义形参来接收window对象;
(5)、沙箱模式的应用:
1、框架封装;
2、组件;
3、插件;
17、调用模式
1、函数调用模式;
function fn(){
在function中的this指window对象;
}
2、对象调用模式;
var obj = {
sayHi:function(){
}
}
obj.sayHi();
3、构造函数调用模式;
var obj = new Object();
4、上下文(context)调用模式;
lvalue 左值;
rvalue 右值;(右值不可作为左值赋值)
两种方式: 1、apply;
2、call;
function.apply(obj , [a,b,c]);
function.call(obj , a, b, c, d ...);
tips:obj为该函数希望this指向的对象,a,b,c,d等为传入函数的实参;
tips:两者区别在于传参的形式不同,
apply在参数不确定的时候使用,call在参数个数确定的时候使用;
tips:apply的参数都放在了数组里,这个特点使得apply很常用;
如果obj传入的类型为简单数据类型, 会自动转换成复杂数据类型;
eg:
test.apply(undefined);
test.apply(null); this会指向window;
eg:
将伪数组变为真数组
var fakeArr = {
length:3,
0:a,
1:b,
2:c
}
var realArr = [];
realArr.push.apply(realArr , fakeArr);
//这时候,fakeArr的数据内容将会存储到realArr中,变成了真数组;
tips:核心用法就是apply会自动将数组拆开,并将其作为参数来调用方法;
eg:
function Animal(){}
function Dog(){
Animal.call(this);
}
var d = new Dog();
console.log(d);
tips:在Dog函数的内部,使用call方法将Animal的this指向实例化对象;
18、创建对象的模式
(1)、工厂模式
eg:
function createPerson(name, age){
var obj = {};
obj.name = name;
obj.age = age;
console.log(this);
return obj;
}
var p = createPerson("张学友", 50);
var p1 = createPerson("郭富城", 50);
tips:工厂模式的特点为,类似于构造函数,但是工厂模式的函数内部自建了一个对象,通过传递参数给对象赋值,最后将该对象作为返回值返回;
(2)、构造函数模式(constructor)
eg:
function Person(name, age){
this.name = name;
this.age = age;
}
var p = new Person("王力宏", 30);
(3)、寄生模式
eg:
function createPerson(name, age){
var obj = {};
obj.name = name;
obj.age = age;
console.log(this);
return obj;
}
var p = new createPerson("刘德华", 60);
tips:寄生模式和工厂模式的区别在于,创建对象的方式寄生模式用new方法,工厂模式给函数直接传参;
19、数组遍历(forEach和map)
(1)、forEach
arr.forEach(value , index , arr){};
tips:value为数组元素,index为元素索引,arr为当前遍历的数组(一般不加);
(2)、map(映射)
arr.map(value , index , arr){};
tips:map的不同之处在于map有返回值,在其中所做的操作返回的结果都会存到一个新的数组,这个新的数组会作为返回值return;
eg:
var arr = [a,b,c,d];
var strArr = arr.map(String);
tips:最后的结果strArr将会是以字符串组成数组;
20、严格模式
"use strict"
'use strict'
1、在严格模式下声明变量 var 不可以省略;
2、在严格模式下形参名不允许重复;
3、早期严格模式下,对象的属性名是不可以重复的,
但是在ECMAScript 6中,支持了这种写法!
4、八进制在严格模式下不允许使用;
eg:
var a = 010;
console.log(a);
tips:a赋值的时候首位为0表明该数为八进制;
5、在严格模式下,eval函数具有自己的作用域
eg:
eval("var a = 10; console.log(a)");
console.log(a);
21、Object.defineProperty
Object.defineProperty(需要添加属性的对象 , "属性名" , {
writable:true,
enumerable:true,
configurable:true,
value:"属性值";
})
(1)、可写性(writable): 默认为false,不能被赋值
value 设置属性的值;
· setter 和 getter
如果只有setter 只写属性 只能赋值,不能获取值
如果只有getter 只读属性 只能读取,不能赋值
tips:setter 和 getter一般不和writable还有 value一起使用;
getter 和 setter可以做校验数据的操作!
tips:意即是说,有了writable和value就不会有setter和getter;
tips:在用getter和setter时,写成get或set;
(2)、可遍历性(enumerable): 默认为false, 不能被遍历;
(3)、可删除性(configurable): 默认为false,不能被删除;
eg:
Object.defineProperty(obj, "job", {
enumerable: true,
configurable: true,
get:function () {
return jobValue;
},
set:function(value){
jobValue = value;
}
});
eg:
Object.defineProperty(obj, "job", {
writable: true,
enumerable: true,
configurable: true,
value: "singer",
});
22、面向对象编程细节
1、面向对象编程, 主要分为两块,第一部分为构造函数,第二部分为原型;
2、在构造函数中存放一些对于对象初始化的操作,对象需要用到的方法尽量保存到原型中;
3、构造函数初始化时调用必要的方法对实例化对象进行初始化,执行代码可以写成一个总类,细类写在原型中,原型提供总类初始化的方法;
eg:
在构造函数中只写一个this.init();
在原型中,init方法调用当前原型的其他方法,从而实现了调用方法的封装;
4、在面向对象封装完毕后,构造函数和原型可以用沙箱模式进行封装,这样创建出来的函数不会对外界产生影响,安全性较高;
5、在写原型的时候,要手动的将原型的constructor指向自定义的构造函数;
5、创建出来的沙箱模式也需要给外界提供接口,便于在外界调用;
eg:
(function (w){
function jQuery(){}
jQuery.prototype = {
constructor:jQuery, //——→手动将构造函数指向jQuery
init:function (){
this.add();
this.push();
},
add:function(){},
push:function(){}
}
window.jQuery = window.$ = jQuery;
})(window)