跳到主要内容

原型与作用域深入解析

原型

原型 prototypefunction 对象的一个属性,同时 prototype 本身也是一个对象。

function Handphone() {
// prototype
}
console.log(Handphone.prototype);

prototype 定义了构造函数创建的每个对象的公共祖先

以下示例中,hp1.romhp2.ram 都可以访问到原型上的属性。所有通过该构造函数创建的对象,都可以继承原型上的属性和方法。

function Handphone(color, brand) {
this.color = color;
this.brand = brand;
this.screen = '4:3';
this.system = 'Android';
}
Handphone.prototype.rom = '64G';
Handphone.prototype.ram = '6G';

var hp1 = new Handphone('red', 'Xiaomi');
var hp2 = new Handphone('black', 'Huawei');

console.log(hp1.rom);
console.log(hp2.ram);

原型上的属性和对象上的属性

访问对象上的属性时,如果不存在,会继续查找原型上的属性。

function Handphone(color, brand) {
this.color = color;
this.brand = brand;
this.screen = '4:3';
this.system = 'Android';
}
Handphone.prototype.rom = '64G';
Handphone.prototype.ram = '6G';
Handphone.prototype.screen = '16:9';

var hp1 = new Handphone('red', 'Xiaomi');
var hp2 = new Handphone('black', 'Huawei');

console.log(hp1.screen);
console.log(hp2.rom);

处理冗余和耦合

在上述示例中,每次 new 时,Handphone 函数内部都会重复赋值 screenramrom,这导致代码冗余和耦合。

为减少冗余,可以将 screensystem 挂载到原型上。

需要传参的属性写在构造函数内部,固定不变的属性写在原型上,通过继承 prototype 来共享。

通常,JavaScript 中的方法挂载到原型上,因为方法不常变更。属性则需要根据需要在构造函数中传递。

function Handphone(color, brand) {
this.color = color;
this.brand = brand;
}

Handphone.prototype.rom = '64G';
Handphone.prototype.ram = '6G';
Handphone.prototype.screen = '4:3';
Handphone.prototype.system = 'Android';

var hp1 = new Handphone('red', 'Xiaomi');
var hp2 = new Handphone('black', 'Huawei');

console.log(hp1.screen);
console.log(hp2.screen);
function Handphone(color, brand) {
this.color = color;
this.brand = brand;
}

Handphone.prototype.rom = '64G';
Handphone.prototype.ram = '6G';
Handphone.prototype.screen = '4:3';
Handphone.prototype.system = 'Android';
Handphone.prototype.call = function () {
console.log('I am calling someone');
};

var hp1 = new Handphone('red', 'Xiaomi');
var hp2 = new Handphone('black', 'Huawei');

hp1.call();
hp2.call();

开发时建议的写法

建议将原型定义为一个对象,以便更清晰地管理属性和方法。

function Handphone(color, brand) {
this.color = color;
this.brand = brand;
}

Handphone.prototype = {
rom: '64G',
ram: '6G',
screen: '4:3',
system: 'Android',
call: function () {
console.log('I am calling someone');
},
};

var hp1 = new Handphone('red', 'Xiaomi');
var hp2 = new Handphone('black', 'Huawei');

hp1.call();
hp2.call();

对原型的增删改

通过实例化的对象,无法直接对原型进行增删改。

function Test() {}

Test.prototype.name = 'prototype';
var test = new Test();

console.log(Test.prototype);
test.num = 1;
delete test.name;
test.name = 'proto';
console.log(Test.prototype, test);
function Handphone(color, brand, system) {
this.color = color;
this.brand = brand;
this.system = system;
}

Handphone.prototype = {
rom: '64G',
ram: '6G',
screen: '4:3',
call: function () {
console.log('I am calling someone');
},
};

var hp1 = new Handphone('black', 'iPhone', 'iOS');
console.log(hp1);
console.log(hp1.constructor);

Constructor

构造器 constructor 指向构造函数本身。也就是说,构造函数通过 constructor 创建对象。

function Handphone(color, brand, system) {
this.color = color;
this.brand = brand;
this.system = system;
}

var hp1 = new Handphone('black', 'iPhone', 'iOS');

console.log(Handphone.prototype);

修改指定的构造函数

function Telephone() {}

function Handphone(color, brand, system) {
this.color = color;
this.brand = brand;
this.system = system;
}

Handphone.prototype = {
constructor: Telephone,
};

var hp1 = new Handphone('black', 'iPhone', 'iOS');

console.log(Handphone.prototype);

Prototype

实例化后,每个对象都有一个原型 Prototype,它属于实例对象,而非构造函数本身。

function Car() {}
Car.prototype.name = 'Benz';
var car = new Car();
console.log(car);


this 没有 name 属性时,会继续查找实例对象的原型 __proto__,如果原型中存在 name,则打印该值。

function Car() {}
Car.prototype.name = 'Benz';
var car = new Car();
console.log(car.name);

__proto__ 可以被改动

__proto__ 是每个实例对象的原型的引用容器,用于指向 prototype。虽然可以修改 __proto__,但不建议轻易改动。

function Person() {}
Person.prototype.name = '张三';

var p1 = {
name: '李四',
};

var person = new Person();

console.log(person.name);
person.__proto__ = p1;
console.log(person.name);

对属性的重写

在 JavaScript 中,建议使用单引号以避免与后端产生冲突,有时需要转义。

function Car() {}
Car.prototype.name = 'Mazda';
var car = new Car();

Car.prototype.name = 'Benz';
console.log(car.name);

实例化前后

实例化后重写 prototype

Car.prototype.name = 'Benz';

function Car() {}

var car = new Car();

Car.prototype = {
name: 'Mazda',
};

console.log(car.name);

实例化前重写 prototype

Car.prototype.name = 'Benz';

function Car() {}

Car.prototype = {
name: 'Mazda',
};

var car = new Car();

console.log(car.name);

Window 和 Return

window.a 相当于将 a 置于全局作用域。

function abc() {
window.a = 3;
}
abc();

console.log(a);

window 和 return 都可以形成闭包

// 使用 return
function test1() {
var a = 1;

function plus1() {
a++;
console.log(a);
}
return plus1;
}

var plus = test1();
plus();
plus();
plus();

// 使用 window
function test2() {
var a = 1;

function add() {
a++;
console.log(a);
}
window.add = add;
}
test2();

add();
add();
add();

window 和 return 的区别

window.变量 将函数放到了全局作用域,而 return 函数 则将函数赋值给了一个变量,如果未被接收,函数会被自动回收。

// 使用 return 的立即执行函数
var add = (function () {
var a = 1;

function add() {
a++;
console.log(a);
}
return add;
})();

add();
add();
add();

// 使用 window 的立即执行函数
(function () {
var a = 1;

function add() {
a++;
console.log(a);
}
window.add = add;
})();

add();
add();
add();

JS 插件的写法

通过立即执行函数隔离作用域,防止变量污染。

(function () {
function Test() {}
window.Test = Test;
})();

var test = new Test();

在前面添加引号是约定俗成,防止忘记添加 ;,确保立即执行函数正常执行,并能快速识别立即执行函数。

(function () {
var Component = function () {};
})();

练习

构造函数接收数字类型的参数,完成数字相加和相乘的功能

function Compute() {
var result = 0;
this.plus = function () {
result = 0;
result = loop(arguments, 'add', result);
};
this.times = function () {
result = 1;
result = loop(arguments, 'mul', result);
};

function loop(args, method, res) {
for (var i = 0; i < args.length; i++) {
var item = args[i];
if (method === 'add') {
res += item;
} else if (method === 'mul') {
res *= item;
}
}

console.log(res);
return res;
}
}

var compute = new Compute();
compute.plus(1, 2, 3); // 输出 6
compute.times(1, 2, 4); // 输出 8