一、原型链(家族族谱)
概念:JS里面的对象可能会有父对象,父对象还会有父对象,。。。。。祖先
- 根本:继承
属性:对象中几乎都会有一个__proto__属性,指向他的父对象
意义:可以实现让该对象访问到父对象中相关属性
- 根对象:Object.prototype
var arr=[1,3,5]
arr.__proto__:Array.prototype
arr.__proto__.__proto__就是找到了根对象
function Animal(){} var cat=new Animal(); //cat.__proto__:Animal.prototype //cat.__proto__.__proto__:根对象
错误的理解:在js中,万物继承自Object? -->在js中万物继承自Object.prototype
二、作用域
(一)、变量作用域
变量作用域的概念:就是一个变量可以使用的范围
JS中首先有一个最外层的作用域:称之为全局作用域
JS中还可以通过函数创建出一个独立的作用域,其中函数可以嵌套,所以作用域也可以嵌套
var age=18; //age是在全局作用域中声明的变量:全局变量 function f1(){ console.log(name); //可以访问到name变量 var name="周董" //name是f1函数内部声明的变量,所以name变量的作用域就是在f1函数内部 console.log(name); //可以访问到name变量 console.log(age); //age是全局作用域中声明的,所以age也可以访问 } console.log(age); //也可以访问
//-->1级作用域 var gender="男"; function fn(){ console.log(age); //因为age是在fn作用域内声明的 //age:undefined:既然有值就是可以访问 console.log(height);//height不是在该作用域内部声明的,所以不能访问 //-->2级作用域 return function(){ //-->3级作用域 var height=180; } var age=5; } //注意:变量的声明和赋值是在两个不同时期的 function fn(){ console.log(age); //undeinfed var age=18; console.log(age); //18 } //fn函数执行的时候,首先找到函数内部所有的变量、函数声明,把他们放在作用域中,给变量一个初始值:undefined -->变量可以访问 //逐条执行代码,在执行代码的过程中,如果有赋值语句,对变量进行赋值 function fn(){ var age; //初始值:undefined console.log(age); //undeinfed age=18; //修改了变量的值 console.log(age); //18 }
(二)、作用域链
由于作用域是相对于变量而言的,而如果存在多级作用域,这个变量又来自于哪里?这个问题就需要好好地探究一下了,我们把这个变量的查找过程称之为变量的作用域链
作用域链的意义:查找变量(确定变量来自于哪里,变量是否可以访问)
简单来说,作用域链可以用以下几句话来概括:(或者说:确定一个变量来自于哪个作用域)
查看当前作用域,如果当前作用域声明了这个变量,就确定结果
查找当前作用域的上级作用域,也就是当前函数的上级函数,看看上级函数中有没有声明
再查找上级函数的上级函数,直到全局作用域为止
如果全局作用域中也没有,我们就认为这个变量未声明(xxx is not defined)
function fn(callback){ var age=18; callback() } fn(function(){ console.log(age); //分析:age变量: //1、查找当前作用域:并没有 //2、查找上一级作用域:全局作用域 //-->难点:看上一级作用域,不是看函数在哪里调用,而是看函数在哪里编写 //-->因为这种特别,我们通常会把作用域说成是:词法作用域 })
举例1:
var name="张三"; function f1(){ var name="abc"; console.log(name); //abc } f1();
举例2:
var name="张三"; function f1(){ console.log(name); //underfind var name="abc"; } f1();
举例3:
var name="张三"; function f1(){ return function(){ console.log(name); //underfind } var name="abc"; } var fn=f1(); fn();
举例4:
var name="张三"; function f1(){ return { say:function(){ console.log(name); //underfind var name="abc"; } } } var fn=f1(); fn.say()
三、闭包
(一)、认识闭包
var divs=document.getElementsByTagName("div");
for (var i = 0; i < divs.length; i++) {
const element = divs[i];
element.οnclick=function(){
//i是来自于全局作用域
alert(i)
}
}
// 执行完for循环之后,i的值已经变成了5
闭包的解决
var divs=document.getElementsByTagName("div");
for (var i = 0; i < divs.length; i++) {
const element = divs[i];
//闭包的解决方案
element.οnclick=(function(j){
return function(){
//i是来自于全局作用域
alert(j)
}
})(i);
}
例一:
function fn(){
var a=5;
return function(){
a++;
console.log(a); //a变量肯定是可以访问的
}
}
var f1=fn(); //f1指向匿名函数
f1(); //6
f1(); //7
f1(); //8
//代码执行到8行fn函数执行完毕,返回匿名函数
// -->一般认为函数执行完毕,变量就会释放,但是此时由于js引擎发现匿名函数要使用a变量,所以a变量并不能得到释放,而是把a变量放在匿名函数可以访问到的地方去了
// -->a变量存在于f1函数可以访问到的地方,当然此时a变量只能被f1函数访问
例二:
function fn(){
var a=5;
return function(){
a++;
console.log(a); //a变量肯定是可以访问的
}
}
var f1=fn(); //f1指向匿名函数
f1(); //6
f1(); //7
f1(); //8
//把a变量的值放在f1函数可以访问到的地方
var f2=fn();
f2(); //6
f2(); //7
f2(); //8
//又一次执行了fn,又初始化了一个新的a变量,值为5;返回匿名函数f2,并且把新的a变量放在了f2可以访问到的地方
var f3=fn();
f3(); //6
//又一次执行了fn,又初始化了一个新的a变量,值为5;返回匿名函数f2,并且把新的a变量放在了f2可以访问到的地方
例三:
function q1(){
var a={};
return a;
}
var r1=q1();
var r2=q1();
console.log(r1==r2); //fasle
function q2(){
var a={}
return function(){
return a;
}
}
var t3=q2();//创建一个新的a对象,把a对象放在t3可以访问到的位置
var o5=t3(); //返回值a就是那个a
var w3=q2();//创建了一个新的a对象,把新的a对象放在w3可以访问到的位置
var o8=w3();//此时获取到的是一个新的a对象
console.log(o5==o8); //false
(二)、闭包的应用场景
-
模块化
-
防止变量被破坏
//模块化思想:也是一种设计模式
var ktv=(function KTV(){
//为了保护leastPrice变量,将它放在函数内部
var leastPrice=1000;
var total=0;
return {
//购物
buy:function(price){
total+=price;
},
//结账
pay:function(){
if(total<leastPrice){
console.log('请继续购物');
}else{
console.log('欢迎下次光临');
}
},
editLeast:function(id,price){
if(id===888){
leastPrice=price;
console.log("现在最低消费金额为:",leastPrice);
}else{
console.log('权限不足');
}
}
}
})()
//假设:来了朋友要来唱K
//——>可能老板需要去修改最低消费的金额
//-->但是并不能让老板直接去修改leastPrice,或者说不能把leastPrice作为全局变量
(三)、闭包问题的产生原因
- 函数执行完毕后,作用域中保留了最新的a变量的值
function f1(){
var a=5;
return function(){
a++;
console.log(a);
}
}
var q1=f1();
//要想释放q1里面保存的a,只能通过释放q1
q1=null; //q1=undefined
四、函数的四种调用方式
- 在`ES6之前`,函数内部的this是由该函数的调用方式决定的
- 在ES6的箭头函数之前的时代,想要判断一个函数内部的this指向谁,就是根据下面的四种方式来决定的
-
函数内部的this跟大小写、书写位置无关
(一)、函数调用
- 函数内部的this指向window
例一:
var age=18;
var p={
age:15,
say:function(){
console.log(this.age); //18
}
}
var f1=p.say; //f1是函数
f1(); //函数调用-->this:window -->this.age=18
例二:
function Person(name) {
this.name = name;
}
Person.prototype = {
constructor: Person,
say: function () {
console.log(this.name); //abc
}
}
//函数的第一种调用方式:函数调用
// -->函数内部的this指向window
// -->window对象中的方法都是全局函数,window对象中的属性都是全局变量
Person("abc");
var p1=Person.prototype.say
p1()
例三:
function fn() {
console.log(this.age) //undefined
}
fn();
(二)、方法调用
- 函数内部的this指向调用该方法的对象
例一:
function Person() {
this.age = 20;
}
Person.prototype.run = function () {
console.log(this.age);
}
var p1 = new Person();
p1.run();
//打印结果:20
例二:
var p2={
height:180,
travel:function(){
console.log(this.height);
}
}
p2.travel() //打印结果:180
例三:
var clear=function(){
console.log(this.length);
}
var length=50;
var tom={
c:clear,
length:100
};
tom.c(); //这里是方法调用的方式
//打印this.length 是50 还是100?
//-->相当于:this是指向window还是指向tom呢?
// -->结果为:100
// -->this:tom
//结论:由于clear函数被当成tom.c()这种方法的形式来进行调用,所以函数内部的this指向调用该方法的对象:tom
var tony={ d:clear,length:30 };
tony.d();
//方法调用的方式,所以clear函数内部的this指向tony的,
(三)、构造函数的调用方式(new调用)
- this指向构造函数的实例
例一:
function fn(name){
this.name=name;
}
//通过new关键字来调用的,那么这种方式就是构造函数的构造函数的调用方式,那么函数内部的this就是该构造函数的实例
var _n=new fn("小明"); //_n有个name属性,值为:小明
例二:
function jQuery(){
var _init=jQuery.prototype.init;
//_init就是一个构造函数
return new _init();
}
jQuery.prototype={
constructor:jQuery,
length:100,
init:function(){
//this可以访问到实例本身的属性,也可以访问到init.prototype中的属性
//这里的init.prototype并不是jQuery.prototype
console.log(this.length);
//正确答案:undefined
//变量声明了未赋值才是undefind、属性不存在也是undefined
}
}
例三:
function jQuery(){
var _init=jQuery.prototype.init;
//_init就是一个构造函数
return new _init();
}
jQuery.prototype={
constructor:jQuery,
length:100,
init:function(){
//this指向init构造函数的实例
//-->1、首先查看本身有没有length属性
//-->2、如果本身没有该属性,那么去它的原型对象中查找
//-->3、如果原型对象中没有,那么就去原型对象的原型对象中查找,最终一直找到根对象(Object.prototype)
//-->4、最终都没有找到的话,我们认为该对象并没有该属性,如果获取该属性的值:undefined
console.log(this.length); //100
}
}
var $init=jQuery.prototype.init;
//修改了init函数的默认原型,指向新原型
$init.prototype=jQuery.prototype;
jQuery();
(四)、上下文调用方式(call、apply、bind)
-
call方法
call方法的第一个参数:
1、如果是一个对象类型,那么函数内部的this指向该对象
2、如果是undefined、null,那么函数内部的this指向window
3、如果是数字-->this:对应的Number构造函数的实例
--> 1 --> new Number(1)
4、如果是字符串-->this:String构造函数的实例
--> "abc" --> new String("abc")
5、如果是布尔值-->this:Boolean构造函数的实例
--> false --> new Boolean(false)
function f1(){
console.log(this);
}
//call方法的第一个参数决定了函数内部的this的值
f1.call([1,3,5])
f1.call({age:20,height:1000})
f1.call(1)
f1.call("abc")
f1.call(true);
f1.call(null)
f1.call(undefined);
-
apply方法
1、上述call的代码apply完全可以替换
2、call和apply都可以改变函数内部的this的值
3、不同的地方:传参的形式不同
function toString(a,b,c){
console.log(a+" "+b+" "+c);
}
toString.call(null,1,3,5) //"1 3 5"
toString.apply(null,[1,3,5])//"1 3 5"
-
bind方法
bind是es5中才有的(IE9+)
例一:
var obj = {
age:18,
run : function(){
console.log(this); //this:obj
var _that=this;
setTimeout(function(){
//this指向window
console.log(this.age);
console.log(_that.age);
//undefined是正确的
},50);
}
}
例二:
var obj5 = {
age:18,
run : function(){
console.log(this); //this:obj5
setTimeout((function(){
console.log(this.age);
}).bind(this),50); //this:obj5
//通过执行了bind方法,匿名函数本身并没有执行,只是改变了该函数内部的this的值,指向obj5
}
}
obj5.run();
例三:
//bind基本用法
function speed(){
console.log(this.seconds);
}
//执行了bind方法之后,产生了一个新函数,这个新函数里面的逻辑和原来还是一样的,唯一的不同是this指向{ seconds:100 }
var speedBind = speed.bind({ seconds:100 });
speedBind(); //100
例四:
(function eat(){
console.log(this.seconds);
}).bind({ seconds:360 })() //360
例五:
var obj={
name:"西瓜",
drink:(function(){
//this指向了:{ name:"橙汁" }
console.log(this.name);
}).bind({ name:"橙汁" })
}
obj.drink(); //"橙汁"