суббота, 12 июня 2010 г.

Сжато и без литья воды о JavaScript

Конспект - результат осмысления языка. Естественно, пользовался чужими наработками, местами прямо копировал себе код. На авторство не претендую, писал в первую очередь для приведения собственных знаний в порядок.


// * Переменные *
// $ - это допустимый символ в имени переменной

// [ ] - это массив:
var $data = [1, 2, 7, '1sdf'];

// { } - это хэш
var $hash = {
   a : 1,
   b : 'ZOO',
   c : function() { }
};

// Переменные объявленные с использованием var имеют локальную область действия,
// без - глобальную (в браузере принадлежат объекту window)


// * Функции *

// Функция:
function foo($str) {
  alert($str);
}

// Функция как переменная:
var $bar = function() {
   foo('Hello');
}; // объект справа от знака равенства является анонимной функцией

// А также, функции в JavaScript являются объектами первого класса:
var $buzz = function($lambda) {
  return $lambda();
};
$buzz($bar);

// В действительности запись:
function foo() { }
// является сокращённой записью:
foo = function() { };
// что является сокращением:
foo = new Function("arg1", "arg2", "body");

// Внутри функции можно получить ссылку на саму себя так:
var factorial = function(x) {
  // используя имя функции
  return x == 1 ? 1 : x + factorial(x - 1);
};
// можно и так:
var factorial = function(x) {
  // arguments.callee
  return x == 1 ? 1 : x + arguments.callee(x - 1);
};
// а можно и так:
var factorial = function f(x) { 
    // локальное имя позволяет сослаться на себя изнутри
  return x == 1 ? 1 : x + f(x - 1);
};


// JavaScript поддерживает замыкания:
function outer() {
  var x = 7; // лексическая переменная захватывается замыканием
  var $closure = function() {
    alert(x);
  }; // на замыканиях основана инкапсуляция данных
}

// Каждое выполнение функции хранит все переменные в специальном объекте [[scope]]
// недоступном для программиста. При создании вложенной функции ей передаётся
// ссылка на внешний [[scope]]. Замыкание, это когда внешний [[scope]] остаётся
// жить после завершения внешней функции. JavaScript всегда хранит внешние 
// [[scope]] до завершения работы внутренних функций, чтобы внутренние функции
// могли найти переменные, если они не найдены в собственных [[scope]]. Все функции
// в JavaScript являются замыканиями!

// Переменные попадают в замыкание по ссылке, а не по значению, поэтому в отличии
// от перла
function create() {
  var $arr = [];
  for(var $i=0; $i<100; $i++) {
    $arr[$i] =  
      function() { alert($i * $i); }; // => 10000
  }
  return $arr;
}
// этот код вернёт массив функций показывающих одно и тоже значение (100*100),
// поэтому в подобных случаях делают двойное замыкание:
function create() {
  var $arr = [];
    for(var $i=1; $i
    arr[$i] = function(x) { // в x копируется значение i
          return function() { alert(x*x); };
    } ($i); // вызов свежесозданной функции
   }
  return arr;
}

// Функции можно добавить свойство:
function Zeppelin() { }
Zeppelin.$counter = 0;
// используя свойства получаем аналог статических переменных внутри функций:
function Zeppelin() {
  var $self = arguments.callee;
  $self.$counter++;
  alert($self.$counter);
}


// * ООП *

// Функция создаёт объект, если вызывается с new. В этом случае она является
// конструктором объекта
function Lizard() {
  this.$color = 'green';
}
var $speedy = new Lizard(); // Lizard - конструктор для $speedy
alert($speedy.$color); // => green

// this отличается от this в C#, C++, e.t.c. В JavaScript this привязывается к
// контексту вызова есть 4 случая:

// 1, если функция вызывается через new как конструктор объекта: this указывает на
// создаваемый объект
function Lizard() {
  this.$color = 'green';
}
// 2. Если функция запущена как метод объекта, this будет ссылкой на этот объект
var $lazy = {
  $speed : 10
}; // это хэш
var $jazz = function() {
  alert(this.$speed);
}; // это функция
$lazy.showSpeed = $jazz; // здесь объекту создали метод showSpeed
$lazy.showSpeed(); // => 30
// 3. Если функция вызывается через call или apply, this указывает на объект 
// переданный в call / apply
function sum(a, b) {
  this.c = a + b;
}
var obj = {};
sum.call(obj, 1, 2); // obj.c = 3
sum.apply(obj, [1, 2]); // obj.c = 3
// 4. в просто вызове функции, this становится равным глобальному объекту. 
// В браузере это windows.


// Инкапсуляция

// Инициализацию данных предпочтительно размещать в конструкторе
var MyClass = function() {
  this.public = function() { }; // доступное снаружи свойство
  var private = function() { }; // закрытое свойство
};
var $obj = new MyClass();
$obj.public(); // корректно
$obj.private(); // ошибка


// Наследование

// ООП построено на наследовании объектов, а не классов. Для понимания наследования
// надо знать, что такое прототип. Прототип - это объект ассоциированный с 
// функцией-конструктором объекта. Сама функция не использует ассоциированный
// прототип. Но созданные функцией объекты содержат заданные в прототипе свойства:
function Rock() { }
Rock.prototype = [1, 5, 13]; // прототипом сейчас является массив
var $guitar = new Rock();
alert($guitar[2]); // => 13

var Blizzard = function() { };
Blizzard.prototype = {
  Z : 7,
  PI : 3.14159
};
var diablo = new Blizzard(); // Blizzard это функция-конструктор
alert(Blizzard.PI); // => "undefined"
alert(diablo.Z); // => 7

// Прототип это скрытая ссылка [[prototype]] недоступная программисту, 
// принадлежащая созданному объекту. Значением этой ссылки является доступное для
// программиста свойство prototype у функции-конструктора в момент вызова new.

// Важным свойством прототипа является то, что если вызывается какое-то свойство
// объекта и оно отсутствует у самого объекта, начинается рекурсивный поиск в 
// объекте на который ссылается прототип. Соответственно, присвоив прототипу
// базовый объект, получаем наследование:

var Car = function() {
  this.speed = 120;
};
var Supercar = function() { };
Supercar.prototype = new Car; // Здесь мы сделали Supercar наследником Car
var $ford = new Supercar();
alert($ford.speed); // => 120

// Указанный способ реализации наследования имеет два недостатка:
// 1 - необходимость создания базового объекта, 
// 2 - вытекающий из первого, необходимость учёта 
// функцией-конструктором вариант своего использования только для назначения 
// прототипа. Гибкость концепции прототипов позволяет неколькими способами
// реализовать наследование, ниже элегантный, но далеко не тривиальный для 
// понимания способ реализовать наследование, лишённый указанных выше недостатков:
function extend(Child, Parent) {
  var F = function() { };
  F.prototype = Parent.prototype;
  Child.prototype = new F();
  Child.prototype.constructor = Child;
  Child.super = Parent.prototype;
}
function Money(summ) {
  this.$ammount = summ;
}
function Dollar() {
   // вызов конструктора родителя с передачей ему аргументов
   Dollar.super.constructor.call( 
    this, arguments[0]);
   // аргументы можно передавать как угодно: массивом arguments либо набором
   // переменных, разделённых запятой - всё это придёт в базовый конструктор,
   // в данном случае в Money( ... )
}
extend(Dollar, Money);
var $money = new Money(1000);
var $dollar = new Dollar(3000);
alert($money.$ammount); // => 1000
alert($dollar.$ammount); // => 3000

// Альтернативный способ создания объектов и наследования основан на добавление
// нужных методов "наследника" в "базовый класс":
function Base() {
  this.$E = 2.718281828;
  return {
    compute : function($arg) {
      return $arg * $E;
    }
  };
}
// Потомок
function Derived() {
  var self = Base(); // <- здесь можно было бы передать аргументы в конструктор
                     // "базового класса"
  self.constructor = arguments.callee;
  self.compute = function($arg) { // это пример перекрытия метода в "наследнике"
        return $arg * $E * $E;
  };
  return self;
}
var $d = new Derived;
$d.compute(7); // => 51.7233

// Различие способов состоит в том, что во втором способе теряется функциональность
// оператора instanceоf, который проверяет принадлежность объекта проходя по
// цепочке его прототипов.

// Это далеко не единственные способы организации наследования в JavaScript,
// существует масса различных комбинаций указанных подходов


// Пара слов о манипуляции с obj.constructor

// Function.prototype.constructor или object.constructor
// это ссылка на конструктор объекта, т.е. на функцию вызванную
// с оператором new. object.constructor устанавливается исxодя
// из Function.prototype.constructor на момент вызова new.
//
// Для каждой функции свойство prototype.constructor должно
// указывать на саму функцию, тогда создаваемые объекты будут 
// иметь правильное свойство constructor. Если свойство prototype 
// для функции переназначается на новый объект, надо 
// обязательно установить и свойство constructor:
function NewYork() { }
NewYork.prototype = { 
  population : 10000000, 
  crime : 'high' 
};
NewYork.prototype.constructor = NewYork;

// при установке прототипа с использованием new: 
var City = function() { };
City.prototype = new NewYork;  
// конструктор автоматически установится ссылкой на NewYork

// Статические члены
function Figure() { } 
Figure.prototype = {
  active : true,
  coords : [0, 0]
};
Figure.prototype.constructor = Figure;
var fig1 = new Figure();
var fig2 = new Figure();
fig1.active = false;
fig1.coords[0] = 10;
alert(fig2.active); // => true
alert(fig2.coords[0]); // => 10 (!)
// причина изменения coords[0] у второго объекта (fig2) в том, 
// что при изменении элемента массива coords первым 
// объектом (fig1), сначала был произведён поиск массива для 
// доступа к его элементу внутри объекта fig1, массив не был 
// найден и далее был произведён поиск в прототипе, где он и 
// был найден. Элемeнты находящиеся в прототипе функции 
// разделяются всеми созданными функцией объектами. 
// Причина же неизменности переменной active у fig2 
// заключается в том, что при присвоении свойству в объекте,
// если свойство отсутствует, оно просто создаётся.
// fig1.active = false; создало свойство в fig1,
// alert(fig2.active); вывело свойство из прототипа.

// Итак, все нестатические члены классов желательно создавать в 
// конструкторе, а все статические члены, обращение к которым
// является не присваиванием, т.е. всё, что не является 
// примитивными типами, размещаетса в прототипе:
function Square() {
  this.color = 'green';
  this.active = true;
  this.coords = [0, 0];
}
Square.prototype = {
  getSomeSharedFlag : function() { return true; },
  someSharedHash : { foo : 1, bar : 2 }
};
Square.prototype.constructor = Square;


// Полиморфизм

// Полиморфизм в JavaScript обеспечивается перегрузкой 
// методов в механизме наследования:
function Ship() {
  this.getVolume = function() { return 1000; };
}
function LargeShip() {
  this.getVolume = function() { return 100000; };
}
LargeShip.prototype = new Ship;

function count(ship) {
  alert(ship.getVolume());
}
var ship1 = new Ship;
var ship2 = new LargeShip;

count( ship1 ); // => 1000
count( ship2 ); // => 100000

2 комментария: