08

1、什么是构造函数

1、什么是构造函数

在 JavaScript 中,用 new 关键字来调用的函数,称为构造函数。构造函数首字母一般大写

2、为什么要使用构造函数?

学习每一个概念,不仅要知道它是什么,还要知道为什么,以及解决什么样的问题。

举个例子,我们要录入一年级一班中每一位同学的个人信息,那么我们可以创建一些对象,比如:

 
1 var p1 = { name: 'zs', age: 6, gender: '男', hobby: 'basketball' };
2 var p2 = { name: 'ls', age: 6, gender: '女', hobby: 'dancing' };
3 var p3 = { name: 'ww', age: 6, gender: '女', hobby: 'singing' };
4 var p4 = { name: 'zl', age: 6, gender: '男', hobby: 'football' };

 

像上面这样,我们可以把每一位同学的信息当做一个对象来处理。但是,我们会发现,我们重复地写了很多无意义的代码。比如 name、age、gender、hobby 。如果这个班上有60个学生,我们得重复写60遍。

这个时候,构造函数的优势就体现出来了。我们发现,虽然每位同学都有 name、gender、hobby这些属性, 但它们都是不同的,那我们就把这些属性当做构造函数的参数传递进去。而由于都是一年级的学生,age 基本都是6岁,所以我们就可以写死,遇到特殊情况再单独做处理即可。此时,我们就可以创建以下的函数:

 
1 function Person(name, gender, hobby) {
2     this.name = name;
3     this.gender = gender;
4     this.hobby = hobby;
5     this.age = 6;
6 }

 

当创建上面的函数以后, 我们就可以通过 new 关键字调用,也就是通过构造函数来创建对象了。

1 var p1 = new Person('zs', '男', 'basketball');
2 var p2 = new Person('ls', '女', 'dancing');
3 var p3 = new Person('ww', '女', 'singing');
4 var p4 = new Person('zl', '男', 'football');

 

此时你会发现,创建对象会变得非常方便。所以,虽然封装构造函数的过程会比较麻烦,但一旦封装成功,我们再创建对象就会变得非常轻松,这也是我们为什么要使用构造函数的原因。

在使用对象字面量创建一系列同一类型的对象时,这些对象可能具有一些相似的特征(属性)和行为(方法),此时会产生很多重复的代码,而使用构造函数就可以实现代码复用。

3、构造函数的执行过程
先说一点基本概念。

 

 1 function Animal(color) { 2 this.color = color; 3 } 

当一个函数创建好以后,我们并不知道它是不是构造函数,即使像上面的例子一样,函数名为大写,我们也不能确定。只有当一个函数以 new 关键字来调用的时候,我们才能说它是一个构造函数。就像下面这样:

var dog = new Animal("black");
1
以下我们只讨论构造函数的执行过程,也就是以 new 关键字来调用的情况。

我们还是以上面的 Person 为例。

 
1 function Person(name, gender, hobby) {
2  this.name = name;
3  this.gender = gender;
4  this.hobby = hobby;
5  this.age = 6;
6 }
7 
8 var p1 = new Person('zs', '男', 'basketball');

 

此时,构造函数会有以下几个执行过程:

(1) 当以 new 关键字调用时,会创建一个新的内存空间,标记为 Animal 的实例。

(2)函数体内部的 this 指向该内存

通过以上两步,我们就可以得出这样的结论。

1 var p2 = new Person('ls', '女', 'dancing');  // 创建一个新的内存 #f2
2 var p3 = new Person('ww', '女', 'singing');  // 创建一个新的内存 #f3

 

每当创建一个实例的时候,就会创建一个新的内存空间(#f2, #f3),创建 #f2 的时候,函数体内部的 this 指向 #f2, 创建 #f3 的时候,函数体内部的 this 指向 #f3。

(3) 执行函数体内的代码
通过上面的讲解,你就可以知道,给 this 添加属性,就相当于给实例添加属性。

(4) 默认返回 this

由于函数体内部的this指向新创建的内存空间,默认返回 this ,就相当于默认返回了该内存空间,也就是上图中的 #f1。此时,#f1的内存空间被变量p1所接受。也就是说 p1 这个变量,保存的内存地址就是 #f1,同时被标记为 Person 的实例。

以上就是构造函数的整个执行过程。

4、构造函数的返回值
构造函数执行过程的最后一步是默认返回 this 。言外之意,构造函数的返回值还有其它情况。下面我们就来聊聊关于构造函数返回值的问题。

(1) 没有手动添加返回值,默认返回 this

1 function Person1() {
2  this.name = 'zhangsan';
3 }
4 
5 var p1 = new Person1();

 

按照上面讲的,我们复习一遍。首先,当用 new 关键字调用时,产生一个新的内存空间 #f11,并标记为 Person1 的实例;接着,函数体内部的 this 指向该内存空间 #f11;执行函数体内部的代码;由于函数体内部的this 指向该内存空间,而该内存空间又被变量 p1 所接收,所以 p1 中就会有一个 name 属性,属性值为 ‘zhangsan’。

 1 p1: { 2 name: 'zhangsan' 3 } 

(2) 手动添加一个基本数据类型的返回值,最终还是返回 this

 
 1 function Person2() {
 2  this.age = 28;
 3  return 50;
 4 }
 5 
 6 var p2 = new Person2();
 7 console.log(p2.age);   // 28
 8 p2: {
 9  age: 28
10 }

 

如果上面是一个普通函数的调用,那么返回值就是 50。

(3) 手动添加一个复杂数据类型(对象)的返回值,最终返回该对象

直接上例子

1 function Person3() {
2  this.height = '180';
3  return ['a', 'b', 'c'];
4 }
5 
6 var p3 = new Person3();
7 console.log(p3.height);  // undefined
8 console.log(p3.length);  // 3
9 console.log(p3[0]);      // 'a'

 

再来一个例子

 
1 function Person4() {
2   this.gender = '男';
3   return { gender: '中性' };
4 }
5 
6 var p4 = new Person4();
7 console.log(p4.gender);  // '中性'

 

5构造函数首字母必须大写吗?
大小写都可以

6不用new关键字,直接运行构造函数,是否会出错?如果不会出错,那么,用new和不用new调用构造函数,有什么区别?
使用new操作符调用函数
例子:

1 function Person(name){
2   this.name = name;
3   this.say = function(){
4     return "I am " + this.name;
5   }
6 }
7 
8 var person1 = new Person('nicole');
9 person1.say(); // "I am nicole"

 

用new调用构造函数,函数内部会发生如下变化:

创建一个this变量,该变量指向一个空对象。并且该对象继承函数的原型;
属性和方法被加入到this引用的对象中;
隐式返回this对象(如果没有显性返回其他对象)
用伪程序来展示上述变化:

 1 function Person(name){
 2   // 创建this变量,指向空对象
 3   var this = {}; 
 4   // 属性和方法被加入到this引用的对象中
 5   this.name = name;
 6   this.say = function(){
 7     return "I am " + this.name;
 8   }
 9   // 返回this对象
10   return this;
11 }

 

 

可以看出,用new调用构造函数,最大特点为,this对象指向构造函数生成的对象,所以,person1.say()会返回字符串: “I am nicole”。

小贴士

如果指定了返回对象,那么,this对象可能被丢失。

 1 function Person(name){
 2   this.name = name;
 3   this.say = function(){
 4     return "I am " + this.name;
 5   }
 6   var that = {};
 7   that.name = "It is that!";
 8   return that;
 9 }
10 
11 var person1 = new Person('nicole');
12 person1.name; // "It is that!"

 

直接调用函数

如果直接调用函数,那么,this对象指向window,并且,不会默认返回任何对象(除非显性声明返回值)。

还是拿Person函数为例,直接调用Person函数:

1 var person1 = Person('nicole');
2 person1; // undefined
3 window.name; // nicole

 

可见,直接调用构造函数的结果,并不是我们想要的。

3.小结

为了防止因为忘记使用new关键字而调用构造函数,可以加一些判断条件强行调用new关键字,代码如下:

 1 function Person(name){
 2   if (!(this instanceof Person)) {
 3     return new Person(name);
 4   }
 5   this.name = name;
 6   this.say = function(){
 7     return "I am " + this.name;
 8   }
 9 }
10 
11 var person1 = Person('nicole');
12 console.log(person1.say()); // I am nicole
13 var person2 = new Person('lisa');
14 console.log(person2.say()); // I am lisa

作者:G_zefeng

链接:https://www.jianshu.com/p/7e21e23ffba9
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

2-创建对象的方式有哪些?举例说明

1. 对象字面量方式

var per = {

 name:'zhangsan',
 age:25,
 job:'html',
 sayName:function(){
  alert(this.name);
 }
}
缺点:使用同一个接口创建很多对象,会产生大量的重复代码。

2、工厂模式

由于在ECMAScript中无法创建类,开发人员就发明了一种函数,用函数来封装以特定接口创建对象的细节,如下面的例子:

function createPerson(name,age,job){
 var o = new Object();
 o.name = name;
 o.age = age;
 o.job = job;
 o.sayName = function(){
  alert(this.name);
 }
 return o;
}
var person1 = createPerson('zhang',30,'java');
var person2 = createPerson('zhao',25,'php');
 
缺点:工厂模式虽然解决了创建多个相似对象的问题,但却没有解决对象识别的问题(即怎样知道一个对象的类型)。
 

3、构造函数模式

可以使用构造函数模式将前面的例子重写如下:

function Person(name,age,job){
 this.name= name;
 this.age = age;
 this.job = job;
 this.sayName = sayName();
}
function sayName(){
  alert(this.name);
}
var person1 = createPerson('zhang',30,'java');
var person2 = createPerson('zhao',25,'php');
 
缺点:在全局作用域中定义的函数实际上只能被某个对象调用,这让全局作用域有点名不副实。更让人无法接受的是:如果对象需要定义很多方法,那么就要定义很多全局函数。好在,这些问题可以通过使用原型模式来解决。
 

4、原型模式

我们创建的每个函数都有一个prototype(原型)属性,这个属性是一个指针,指向一个对象,而这个对象的用途是包含可以由特定类型的所有实例共享的属性和方法。使用原型对象的好处是可以让所有的对象实例共享他所包含的属性和方法。

function Person(){}
Person.prototype.name = 'zhang';
Person.prototype.age = '22';
Person.prototype.job = 'html5';
Person.prototype.sayName = function(){
 alert(this.name);
};
var person1 = new Person();
var person2 = new Person();

原型模式也不是没有缺点。首先,它省略了为构造函数传递初始化参数这一环节,结果所有实例在默认情况下都将取得相同的属性值。虽然这会在某种程度上带来一些不方便,但还不是原型的最大问题。原型模式的最大问题是由共享的本性所导致的。

 

5、组合使用构造函数模式和原型模式

组合使用构造函数模式和原型模式,是创建自定义类型的最常见方式。构造函数模式用于定义实例属性,而原型模式用于定义方法和共享的属性。结果,每个实例都会有自己的一份实例属性的副本,但同时又共享着对方法的引用,最大限度的节省了内存。

function Person(){}
Person.prototype = {
 constructor:Person,
 name:'zhang',
 age :'22',
 job :'html5',
 friends:['wang','li'],
 sayName : function(){
  alert(this.name);
 }
};
var person1 = new Person();
var person2 = new Person();
person1.friends.push('zhao');
alert(person1.friends); //'wang,li,zhao'
alert(person2.friends); //'wang,li,zhao'
alert(person1.friends === person2.friends); //true

在上面的例子中,实例属性都是在构造函数中定义的,而由所有实例共享的属性constructor和方法satName()则是在原型中定义的。而修改了person1.friends(向其中添加一个新字符串),并不会影响到person2.friends,因为他们分别引用了不同的数组。

3-js 种实现继承的方式
  1,原型链继承 (有两种实现方式)

(1)Son.prototype = Father.prototype

弊端:Son.prototype.constructor 指向Father,需要手动更改为Son ;Son的实例化对象只能继承Father原型中的方法,无法继承Father本身的属性。

(2)Son.prototype = new Father()

弊端:Son.prototype.constructor 指向Father,需要手动更改为Son;Son的实例化对象共享Father自身的引用类型属性。什么意思呢?下面举个栗子:

function Father(){ this.name = "zs" ; this.arr = [1,2,3] }

function Son(){ }

Son.prototype = new Father()

var s1 = new Son(), s2 = new Son();

s1.arr.push(5);

console.log(s1.arr)--------> [1,2,3,5]

console.log(s2.arr)--------->[1,2,3,5]

看了这里例子就明白了,Son的实例化对象s1,s2继承了Father的属性arr,但是s1,s2是同时指向这一属性的。

2,借助构造函数继承

function Father(){ this.name = "zs"; this.age=38 };

function Son(){ Father.call( this) / Father.apply(this) }

弊端:Son只能继承Father自身的属性,而无法继承Father原型中的方法。

3,组合式继承

将原型链继承与构造函数结合起来

function Father(){ this.name="zs";this.age=38 }

Father.prototype.sayHi = function(){ alert("hello") }

function Son(){ Father.call(this) }

Son.prototype = new Father()

var s = new Son() ;

弊端:通过Father.call() 和 new Father() ,父类构造函数Father被调用了两次。

4,原型式继承

function createObj(o){ function F(){ } F.prototype=o ; return new F() }

var obj = { name:"zs" , age:18, sayHi:function(){ } }

var newObj = createObj( obj );

newObj继承了obj的属性和方法,但是同样出现了共享父类中引用类型属性的问题。

5,经典继承(Es5中的新语法:存在兼容性问题,需要做浏览器的能力检测)

function create(obj){

if(Object.create){ return Object.create(obj) }

else { function F(){ } F.prototype=o ; return new F() }

}

6,寄生式继承(类似于原型式继承)

function createObj(o){ function F(){ } F.prototype=o ; return new F() }

function createObj2(o){ var obj = createObj(o) ; obj.sayHi = function(){ } return obj }

var obj = { name:"zs" , age:18, sayHi:function(){ } }

var newObj = createObj2(obj)

newObj继承了obj的属性和方法,但是同样出现了共享父类中引用类型属性的问题。

7,寄生组合式继承(组合继承+寄生继承)

function createObj(o){ function F(){ } F.prototype=o ; return new F() }

function inheritPrototype(Child, Father) {

var prototype = object(Father.prototype);//创建对象

prototype.constructor = Child;//增强对象

Child.prototype = prototype;//指定对象 }

function Father(name) {

this.name = name;

this.arr = [1, 2, 3, 4]; }

Father.prototype.sayName = function () { console.log("父类原型" + this.name); }

function Child(name, age) { Father.call(this, name); this.age = age; }

inheritPrototype(Child, Father)

Child.prototype.sayAge = function () { console.log(this.age); }

var child1 = new Child() , child2 = new Child();

child1.arr.push(5) ------> [1,2,3,4,5]

child2.arr ------> [1,2,3,4].

优点:可以多重继承 解决两次调用 解决实例共享引用类型的问题 原型链保持不变

4-什么是闭包?有什么作用?

如果一个函数用到了它作用域外面的变量,那么这个变量和这个函数之间的环境就叫闭包

例子

1 var a = 5
2 function xx(){
3     console.log(a)
4 }
5 //5

作用

1.模仿块级作用域

所谓块级作用域就是指在循环中定义的变量,一旦循环结束,变量也随之销毁,它的作用范围只在这一小块

而在JavaScript中没有这样的块级作用域,由于JavaScript不会告诉你变量是否已经被声明,所以容易造成命名冲突,如果在全局环境定义的变量,就会污染全局环境,因此可以利用闭包的特性来模仿块级作用域。

1 function X(num) {
2     (function(){
3         for(var i = 0; i < num.length; i++){
4             num++
5         }
6     }).call() //声明一个函数立即调用以后,浏览器刷新页面会报错,可以用一个小括号把整段函数包起来。
7     console.log(i)//undefined
8 }
2.储存变量

闭包的另一个特点是可以保存外部函数的变量,内部函数保留了对外部函数的活动变量的引用,所以变量不会被释放。

1 function S(){
2     var a = 1
3     return {
4         function(){
5             renturn a
6         }
7     }
8 }
9 var d = S() // 100
3.封装私有变量

我们可以把函数当作一个范围,函数内部的变量就是私有变量,在外部无法引用,但是我们可以通过闭包的特点来访问私有变量。

 1 var person = function(){
 2     //变量作用域为函数内部,外部无法访问
 3     var name = "default";
 4     return {
 5         getName : function(){
 6             return name;
 7         },
 8         setName : function(newName){
 9             name = newName;
10         }
11     }
12 }();
13 print(person.name);//直接访问,结果为undefined
14 print(person.getName()); // default
15 person.setName("abruzzi");
16 print(person.getName()); // abruzzi

5、JS预解析是什么?
这件事,可以先产生一个思考,浏览器为什么会读我们的JS代码?难道就几行字,他就认识了?

其实在浏览器内部,有一块是专门解析JS数据的,我们可以称之为JS 解析器 。

一旦浏览器 识别到了SCRIPT标签,JS解析器就开始干活了。
这里 JS的解析器至少会做两部分的工作:
1)预解析 | 预解析仓库 = “找东西“(找VAR , FUNCTION 以及参数)
2)逐行解读代码

先上一个简单的代码,大家可以想一下 a在这个时候弹出的是什么?为什么

alert(a); //underfind
var a = 10;

bingo! 答案就是underfind但是为什么呢?
就 像我们之前说的,JS 是逐行解读,运行代码的,那么在这之前还有个预解析的过程,
在这个过程中, 所有的变量,在正式运行代码之前,都提前赋了一个值 那就是:未定义。
预解析之后,代码开始运行。
当执行到第一行的时候,他不会先去下面看,他会先到预解析仓库中去看看有没有a,
一看他找到了a,但是a在这个时候是underfined,那么他就输出了underfind.

说完了变量,在看看函数,他弹出的是什么

alert(a);
function a(){
    alert(1)
    };

和之前变量的预解析一样,只不过在这里 a=function(){alert(1)};
在代码执行之前,a就是整个函数块,
那么上面alert(a) 的内容也很明显了,执行代码的时候,浏览器去预解析仓库中找,发现了function a(),在这个时候function a(){} 就是整个函数块内容
那么他输出的就是 a=function a(){alert(1)};

简单的说完了,在来个进阶部分,代码如下:

alert(a);
var a = 1;
alert(a);
function a(){
    alert(2)
};
alert(a);
var a = 3;
alert(a);
function a(){
    alert(4)
};
alert(a);

答案是:
function a(){
alert(4)
}
1;
1;
3;
3;

为什么第三个和第五个 会是1和3呢,因为表达式会改变预解析仓库里的值。

posted @ 2021-02-20 21:38  路~  阅读(94)  评论(0)    收藏  举报